cloudstack-mirror/test/integration/smoke/test_dns_framework_powerdns.py

402 lines
15 KiB
Python

# 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 *
from nose.plugins.attrib import attr
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.pdns_unavailable = False
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}")
# Wait for PDNS to be ready with polling
cls.pdns_url = f"http://{cls.marvin_vm_ip}"
cls.logger.info(f"PDNS endpoint: {cls.pdns_url}:8081")
cls._wait_for_pdns_ready()
@classmethod
def _wait_for_pdns_ready(cls, timeout=90, interval=1):
"""
Poll PDNS API until it responds or timeout is reached.
Logs a message and sets cls.pdns_unavailable if PDNS is not reachable.
"""
import urllib.request
api_url = f"{cls.pdns_url}:8081/api/v1/servers"
cls.logger.info(f"Waiting for PDNS to be ready at {api_url} (timeout: {timeout}s)")
for _ in range(timeout // interval):
try:
req = urllib.request.Request(api_url)
req.add_header("X-API-Key", "supersecretapikey")
resp = urllib.request.urlopen(req, timeout=2)
if resp.status == 200:
cls.logger.info("PDNS is up and running")
cls.pdns_unavailable = False
return
except Exception:
pass
time.sleep(interval)
cls.logger.warning("PDNS did not become ready within timeout; skipping tests")
cls.pdns_unavailable = True
def setUp(self):
if self.__class__.pdns_unavailable:
import unittest
raise unittest.SkipTest("PDNS is unavailable; skipping test")
@attr(tags=["advanced"], required_hardware="true")
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]}")
@attr(tags=["advanced"], required_hardware="true")
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")
@attr(tags=["advanced"], required_hardware="true")
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")
@attr(tags=["advanced"], required_hardware="true")
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}")
@attr(tags=["advanced"], required_hardware="true")
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")
@attr(tags=["advanced"], required_hardware="true")
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")
self._assert_dns("www.example.com", "A", expected="10.1.1.10")
@attr(tags=["advanced"], required_hardware="true")
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")
self._assert_dns("www.example.com", "AAAA", expected="2001:db8::10")
@attr(tags=["advanced"], required_hardware="true")
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")
self._assert_dns("example.com", "MX", contains=["10", "mail.example.com"])
@attr(tags=["advanced"], required_hardware="true")
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")
@attr(tags=["advanced"], required_hardware="true")
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")
@attr(tags=["advanced"], required_hardware="true")
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)}")
@attr(tags=["advanced"], required_hardware="true")
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.dnsapikey = "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)
def _dig(self, name, rtype):
dns_ip = self.__class__.marvin_vm_ip
dns_port = 53
cmd = [
"dig",
f"@{dns_ip}",
"-p",
str(dns_port),
name,
rtype,
"+short"
]
self.logger.info(f"Running: {' '.join(cmd)}")
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.assertEqual(result.returncode, 0, f"dig failed: {result.stderr}")
output = result.stdout.strip().replace("\n", " ")
self.logger.info(f"dig output: {output}")
return output
def _assert_dns(self, name, rtype, expected=None, contains=None):
output = self._dig(name, rtype)
if expected is not None:
self.assertIn(expected, output)
if contains:
for item in contains:
self.assertIn(item, output)
return output