add marvin test for powerdns

This commit is contained in:
Manoj Kumar 2026-04-15 18:57:46 +05:30
parent 6466362552
commit 9d0884a0a6
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
9 changed files with 335 additions and 22 deletions

View File

@ -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";

View File

@ -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() {

View File

@ -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;

View File

@ -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());

View File

@ -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());

View File

@ -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');

View File

@ -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;
}

View File

@ -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();

View File

@ -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)