Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ jobs:
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
run: sudo apt update && sudo apt install --yes tox libev-dev libvirt-dev
- name: Install dependencies
run: sudo apt update && sudo apt install --yes tox libev-dev libvirt-dev pdns-server pdns-backend-pgsql
- name: Unit tests
run: |
tox -e ${{ matrix.python-version }}
Expand All @@ -74,4 +74,4 @@ jobs:
run: sudo apt update && sudo apt install --yes tox libev-dev libvirt-dev
- name: Coverage
run: |
tox -e begin,${{ matrix.python-version }},end
tox -e begin,${{ matrix.python-version }},end
9 changes: 9 additions & 0 deletions etc/powerdns/genesis.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
local-port=5300

launch+=gpgsql
gpgsql-dbname=genesis_core
gpgsql-user=genesis_core
gpgsql-password=genesis_core
gpgsql-host=localhost
# or set several ips explicitly, it'll try to use them in order
# gpgsql-extra-connection-parameters=hostaddr=172.17.0.1,172.17.0.2
11 changes: 11 additions & 0 deletions genesis/images/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,14 @@ sudo cp "$GC_PATH/etc/systemd/genesis-universal-scheduler.service" $SYSTEMD_SERV
sudo systemctl enable gc-user-api gc-orch-api gc-status-api gc-gservice \
genesis-universal-agent \
genesis-universal-scheduler


# Prepare DNSaaS

# Install packages
sudo apt install pdns-backend-pgsql pdns-server -y
Comment thread
akremenetsky marked this conversation as resolved.

sudo rm /etc/powerdns/pdns.d/bind.conf
sudo cp "$GC_PATH/etc/powerdns/genesis.conf" /etc/powerdns/pdns.d/genesis.conf

sudo systemctl enable pdns
Empty file.
99 changes: 99 additions & 0 deletions genesis_core/tests/functional/service/dns/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2025 Genesis Corporation.
#
# All Rights Reserved.
#
# Licensed 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 __future__ import annotations

import logging
import os
import socket
import subprocess
from contextlib import closing
import typing as tp
import uuid as sys_uuid
from urllib.parse import urlparse

import pytest

from restalchemy.tests.functional import consts as ra_c


LOG = logging.getLogger(__name__)

PDNS_BIN = "/usr/sbin/pdns_server"


def find_free_port():
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]


@pytest.fixture()
def pdns_server(user_api, tmp_path_factory: pytest.TempPathFactory):
result = urlparse(ra_c.DATABASE_URI)
if result.scheme != "postgresql":
pytest.skip("Only PostgreSQL is supported for PowerDNS tests")
if not os.path.exists(PDNS_BIN):
pytest.skip(
"PowerDNS server binary not found, dataplane can't be checked"
)

port = result.port

directory = tmp_path_factory.mktemp("pdns")
config_file = directory / "pdns.conf"
port = find_free_port()

config = f"""\
local-port={port}
launch=gpgsql
gpgsql-dbname={result.path[1:]}
gpgsql-user={result.username}
gpgsql-password={result.password}
gpgsql-host={result.hostname}
loglevel=100
query-logging=yes
log-dns-queries=yes
"""
with open(config_file, "w") as f:
f.write(config)

proc = subprocess.Popen(
[
PDNS_BIN,
"--guardian=no",
"--daemon=no",
"--disable-syslog",
"--log-timestamp=no",
"--write-pid=no",
"--socket-dir=" + str(directory),
"--config-dir=" + str(directory),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)

# Check it started successfully
assert not proc.poll(), proc.stdout.read().decode("utf-8")

yield port

proc.terminate()

# Useful to debug if things go wrong, will be shown only on test failure.
LOG.warning("PDNS log:")
LOG.warning(proc.stdout.read().decode("utf-8"))
224 changes: 224 additions & 0 deletions genesis_core/tests/functional/service/dns/test_pdns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Copyright 2025 Genesis Corporation.
#
# All Rights Reserved.
#
# Licensed 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.

import uuid as sys_uuid
import typing as tp

import dns.resolver
import pytest
from gcl_iam.tests.functional import clients as iam_clients

from genesis_core.common import constants as c


DEF_DOMAIN = "core.internal"


class TestDnsApi:

# Utils

@staticmethod
def _cmp_shallow(
left: tp.Dict[str, tp.Any],
right: tp.Dict[str, tp.Any],
):
return all((left[key] == right[key]) for key in left.keys())

@pytest.fixture()
def domain1(
self,
user_api_client: iam_clients.GenesisCoreTestRESTClient,
auth_user_admin: iam_clients.GenesisCoreAuth,
):
domain = {
"uuid": str(sys_uuid.uuid4()),
"name": DEF_DOMAIN,
"project_id": str(c.SERVICE_PROJECT_ID),
}
client = user_api_client(auth_user_admin)
url = client.build_collection_uri(["dns", "domains"])

response = client.post(url, json=domain)
output = response.json()

assert response.status_code == 201
assert self._cmp_shallow(domain, output)
yield output

# DNS

def test_domains_list(
self,
user_api_client: iam_clients.GenesisCoreTestRESTClient,
auth_user_admin: iam_clients.GenesisCoreAuth,
):
client = user_api_client(auth_user_admin)
url = client.build_collection_uri(["dns", "domains"])

response = client.get(url)

assert response.status_code == 200
assert len(response.json()) == 0

def test_domains_add(
self,
user_api_client: iam_clients.GenesisCoreTestRESTClient,
auth_user_admin: iam_clients.GenesisCoreAuth,
domain1: tp.Dict,
pdns_server: int | None,
):
client = user_api_client(auth_user_admin)

# Check SOA Record

url = client.build_collection_uri(
["dns", "domains", domain1["uuid"], "records"]
)

response = client.get(url)
records = response.json()
assert response.status_code == 200
assert len(records) == 1
assert records[0]["type"] == "SOA"
assert records[0]["record"]["name"] == "@"
assert (
records[0]["record"]["primary_dns"]
== "a.misconfigured.dns.server.invalid"
)

if pdns_server:
res = dns.resolver.make_resolver_at("127.0.0.1", port=pdns_server)
answer = res.resolve(DEF_DOMAIN, "SOA")

assert len(answer) == 1
assert (
answer[0].to_text()
== "a.misconfigured.dns.server.invalid. core.internal. 0 10800 3600 604800 3600"
)

# Delete

url = client.build_resource_uri(["dns", "domains", domain1["uuid"]])

response = client.delete(url)

assert response.status_code == 204

url = client.build_collection_uri(["dns", "domains"])

response = client.get(url)

assert response.status_code == 200
assert len(response.json()) == 0

def test_a_record(
self,
user_api_client: iam_clients.GenesisCoreTestRESTClient,
auth_user_admin: iam_clients.GenesisCoreAuth,
domain1: tp.Dict,
pdns_server: int | None,
):
client = user_api_client(auth_user_admin)

data = {
"uuid": str(sys_uuid.uuid4()),
"type": "A",
"ttl": 0,
"record": {"kind": "A", "name": "test", "address": "1.2.3.4"},
}

url = client.build_collection_uri(
["dns", "domains", domain1["uuid"], "records"]
)

response = client.post(url, json=data)
output = response.json()

assert response.status_code == 201
assert self._cmp_shallow(data, output)

url = client.build_resource_uri(
["dns", "domains", domain1["uuid"], "records", data["uuid"]]
)

response = client.get(url)
record = response.json()
assert response.status_code == 200
assert self._cmp_shallow(data, record)

if pdns_server:
res = dns.resolver.make_resolver_at("127.0.0.1", port=pdns_server)
answer = res.resolve(f"test.{DEF_DOMAIN}", "A")

assert len(answer) == 1
assert answer[0].address == "1.2.3.4"

# Delete
response = client.delete(url)

assert response.status_code == 204

def test_txt_record(
self,
user_api_client: iam_clients.GenesisCoreTestRESTClient,
auth_user_admin: iam_clients.GenesisCoreAuth,
domain1: tp.Dict,
pdns_server: int | None,
):
client = user_api_client(auth_user_admin)

data = {
"uuid": str(sys_uuid.uuid4()),
"type": "TXT",
"ttl": 0,
"record": {"kind": "TXT", "name": "test", "content": "a" * 5000},
}

url = client.build_collection_uri(
["dns", "domains", domain1["uuid"], "records"]
)

response = client.post(url, json=data)
output = response.json()

assert response.status_code == 201
assert self._cmp_shallow(data, output)

url = client.build_resource_uri(
["dns", "domains", domain1["uuid"], "records", data["uuid"]]
)

response = client.get(url)
record = response.json()
assert response.status_code == 200
assert self._cmp_shallow(data, record)

if pdns_server:
res = dns.resolver.make_resolver_at("127.0.0.1", port=pdns_server)
answer = res.resolve(f"test.{DEF_DOMAIN}", "TXT")

assert len(answer) == 1
# TXT records may not fit in one UDP frame, so there'll be many
# strings inside
assert (
"".join([i.decode() for i in answer[0].strings]) == "a" * 5000
)

# Delete
response = client.delete(url)

assert response.status_code == 204
2 changes: 2 additions & 0 deletions genesis_core/tests/functional/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def setup_class(cls):
def teardown_class(cls):
cls.drop_all_views()
cls.drop_all_tables(cascade=True)
# Hack for psycopg to finish fast, otherwise we'll need to wait for GC
cls.engine.__del__()
cls.destroy_engine()

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions genesis_core/user_api/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from restalchemy.api import routes

from genesis_core.user_api.api import controllers
from genesis_core.user_api.dns.api import routes as dns_routes
from genesis_core.user_api.em.api import routes as em_routes
from genesis_core.user_api.iam.api import routes as iam_routes
from genesis_core.user_api.config.api import routes as config_routes
Expand Down Expand Up @@ -60,6 +61,7 @@ class ApiEndpointRoute(routes.Route):
__controller__ = controllers.ApiEndpointController
__allow_methods__ = [routes.FILTER]

dns = routes.route(dns_routes.DnsRoute)
health = routes.route(HealthRoute)
iam = routes.route(iam_routes.IamRoute)
em = routes.route(em_routes.ElementManagerRoute)
Expand Down
Empty file.
Empty file.
Loading