Skip to content

Commit 279cf69

Browse files
committed
Introduce basic DNSaaS
Signed-off-by: George Melikov <mail@gmelikov.ru>
1 parent 0a284f4 commit 279cf69

16 files changed

Lines changed: 756 additions & 5 deletions

File tree

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ jobs:
4848
uses: actions/setup-python@v3
4949
with:
5050
python-version: ${{ matrix.python-version }}
51-
- name: Install tox
52-
run: sudo apt update && sudo apt install --yes tox libev-dev libvirt-dev
51+
- name: Install dependencies
52+
run: sudo apt update && sudo apt install --yes tox libev-dev libvirt-dev pdns-server pdns-backend-pgsql
5353
- name: Unit tests
5454
run: |
5555
tox -e ${{ matrix.python-version }}
@@ -74,4 +74,4 @@ jobs:
7474
run: sudo apt update && sudo apt install --yes tox libev-dev libvirt-dev
7575
- name: Coverage
7676
run: |
77-
tox -e begin,${{ matrix.python-version }},end
77+
tox -e begin,${{ matrix.python-version }},end

etc/powerdns/genesis.conf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
local-port=5300
2+
3+
launch+=gpgsql
4+
gpgsql-dbname=genesis_core
5+
gpgsql-user=genesis_core
6+
gpgsql-password=genesis_core
7+
gpgsql-host=localhost
8+
# or set several ips explicitly, it'll try to use them in order
9+
# gpgsql-extra-connection-parameters=hostaddr=172.17.0.1,172.17.0.2

genesis/images/install.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,14 @@ sudo cp "$GC_PATH/etc/systemd/genesis-universal-scheduler.service" $SYSTEMD_SERV
114114
sudo systemctl enable gc-user-api gc-orch-api gc-status-api gc-gservice \
115115
genesis-universal-agent \
116116
genesis-universal-scheduler
117+
118+
119+
# Prepare DNSaaS
120+
121+
# Install packages
122+
sudo apt install pdns-backend-pgsql pdns-server -y
123+
124+
sudo rm /etc/powerdns/pdns.d/bind.conf
125+
sudo cp "$GC_PATH/etc/powerdns/genesis.conf" /etc/powerdns/pdns.d/genesis.conf
126+
127+
sudo systemctl enable pdns

genesis_core/tests/functional/service/dns/__init__.py

Whitespace-only changes.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright 2025 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
from __future__ import annotations
18+
19+
import logging
20+
import os
21+
import socket
22+
import subprocess
23+
from contextlib import closing
24+
import typing as tp
25+
import uuid as sys_uuid
26+
from urllib.parse import urlparse
27+
28+
import pytest
29+
30+
from restalchemy.tests.functional import consts as ra_c
31+
32+
33+
LOG = logging.getLogger(__name__)
34+
35+
PDNS_BIN = "/usr/sbin/pdns_server"
36+
37+
38+
def find_free_port():
39+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
40+
s.bind(("", 0))
41+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42+
return s.getsockname()[1]
43+
44+
45+
@pytest.fixture()
46+
def pdns_server(user_api, tmp_path_factory: pytest.TempPathFactory):
47+
result = urlparse(ra_c.DATABASE_URI)
48+
if result.scheme != "postgresql":
49+
pytest.skip("Only PostgreSQL is supported for PowerDNS tests")
50+
if not os.path.exists(PDNS_BIN):
51+
pytest.skip(
52+
"PowerDNS server binary not found, dataplane can't be checked"
53+
)
54+
55+
port = result.port
56+
57+
directory = tmp_path_factory.mktemp("pdns")
58+
config_file = directory / "pdns.conf"
59+
port = find_free_port()
60+
61+
config = f"""\
62+
local-port={port}
63+
launch=gpgsql
64+
gpgsql-dbname={result.path[1:]}
65+
gpgsql-user={result.username}
66+
gpgsql-password={result.password}
67+
gpgsql-host={result.hostname}
68+
loglevel=100
69+
query-logging=yes
70+
log-dns-queries=yes
71+
"""
72+
with open(config_file, "w") as f:
73+
f.write(config)
74+
75+
proc = subprocess.Popen(
76+
[
77+
PDNS_BIN,
78+
"--guardian=no",
79+
"--daemon=no",
80+
"--disable-syslog",
81+
"--log-timestamp=no",
82+
"--write-pid=no",
83+
"--socket-dir=" + str(directory),
84+
"--config-dir=" + str(directory),
85+
],
86+
stdout=subprocess.PIPE,
87+
stderr=subprocess.STDOUT,
88+
)
89+
90+
# Check it started successfully
91+
assert not proc.poll(), proc.stdout.read().decode("utf-8")
92+
93+
yield port
94+
95+
proc.terminate()
96+
97+
# Useful to debug if things go wrong, will be shown only on test failure.
98+
LOG.warning("PDNS log:")
99+
LOG.warning(proc.stdout.read().decode("utf-8"))
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright 2025 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
import uuid as sys_uuid
18+
import typing as tp
19+
20+
import dns.resolver
21+
import pytest
22+
from gcl_iam.tests.functional import clients as iam_clients
23+
24+
from genesis_core.common import constants as c
25+
26+
27+
DEF_DOMAIN = "core.internal"
28+
29+
30+
class TestDnsApi:
31+
32+
# Utils
33+
34+
@staticmethod
35+
def _cmp_shallow(
36+
left: tp.Dict[str, tp.Any],
37+
right: tp.Dict[str, tp.Any],
38+
):
39+
return all((left[key] == right[key]) for key in left.keys())
40+
41+
@pytest.fixture()
42+
def domain1(
43+
self,
44+
user_api_client: iam_clients.GenesisCoreTestRESTClient,
45+
auth_user_admin: iam_clients.GenesisCoreAuth,
46+
):
47+
domain = {
48+
"uuid": str(sys_uuid.uuid4()),
49+
"name": DEF_DOMAIN,
50+
"project_id": str(c.SERVICE_PROJECT_ID),
51+
}
52+
client = user_api_client(auth_user_admin)
53+
url = client.build_collection_uri(["dns", "domains"])
54+
55+
response = client.post(url, json=domain)
56+
output = response.json()
57+
58+
assert response.status_code == 201
59+
assert self._cmp_shallow(domain, output)
60+
yield output
61+
62+
# DNS
63+
64+
def test_domains_list(
65+
self,
66+
user_api_client: iam_clients.GenesisCoreTestRESTClient,
67+
auth_user_admin: iam_clients.GenesisCoreAuth,
68+
):
69+
client = user_api_client(auth_user_admin)
70+
url = client.build_collection_uri(["dns", "domains"])
71+
72+
response = client.get(url)
73+
74+
assert response.status_code == 200
75+
assert len(response.json()) == 0
76+
77+
def test_domains_add(
78+
self,
79+
user_api_client: iam_clients.GenesisCoreTestRESTClient,
80+
auth_user_admin: iam_clients.GenesisCoreAuth,
81+
domain1: tp.Dict,
82+
pdns_server: int | None,
83+
):
84+
client = user_api_client(auth_user_admin)
85+
86+
# Check SOA Record
87+
88+
url = client.build_collection_uri(
89+
["dns", "domains", domain1["uuid"], "records"]
90+
)
91+
92+
response = client.get(url)
93+
records = response.json()
94+
assert response.status_code == 200
95+
assert len(records) == 1
96+
assert records[0]["type"] == "SOA"
97+
assert records[0]["record"]["name"] == "@"
98+
assert (
99+
records[0]["record"]["primary_dns"]
100+
== "a.misconfigured.dns.server.invalid"
101+
)
102+
103+
if pdns_server:
104+
res = dns.resolver.make_resolver_at("127.0.0.1", port=pdns_server)
105+
answer = res.resolve(DEF_DOMAIN, "SOA")
106+
107+
assert len(answer) == 1
108+
assert (
109+
answer[0].to_text()
110+
== "a.misconfigured.dns.server.invalid. core.internal. 0 10800 3600 604800 3600"
111+
)
112+
113+
# Delete
114+
115+
url = client.build_resource_uri(["dns", "domains", domain1["uuid"]])
116+
117+
response = client.delete(url)
118+
119+
assert response.status_code == 204
120+
121+
url = client.build_collection_uri(["dns", "domains"])
122+
123+
response = client.get(url)
124+
125+
assert response.status_code == 200
126+
assert len(response.json()) == 0
127+
128+
def test_a_record(
129+
self,
130+
user_api_client: iam_clients.GenesisCoreTestRESTClient,
131+
auth_user_admin: iam_clients.GenesisCoreAuth,
132+
domain1: tp.Dict,
133+
pdns_server: int | None,
134+
):
135+
client = user_api_client(auth_user_admin)
136+
137+
data = {
138+
"uuid": str(sys_uuid.uuid4()),
139+
"type": "A",
140+
"ttl": 0,
141+
"record": {"kind": "A", "name": "test", "address": "1.2.3.4"},
142+
}
143+
144+
url = client.build_collection_uri(
145+
["dns", "domains", domain1["uuid"], "records"]
146+
)
147+
148+
response = client.post(url, json=data)
149+
output = response.json()
150+
151+
assert response.status_code == 201
152+
assert self._cmp_shallow(data, output)
153+
154+
url = client.build_resource_uri(
155+
["dns", "domains", domain1["uuid"], "records", data["uuid"]]
156+
)
157+
158+
response = client.get(url)
159+
record = response.json()
160+
assert response.status_code == 200
161+
assert self._cmp_shallow(data, record)
162+
163+
if pdns_server:
164+
res = dns.resolver.make_resolver_at("127.0.0.1", port=pdns_server)
165+
answer = res.resolve(f"test.{DEF_DOMAIN}", "A")
166+
167+
assert len(answer) == 1
168+
assert answer[0].address == "1.2.3.4"
169+
170+
# Delete
171+
response = client.delete(url)
172+
173+
assert response.status_code == 204

genesis_core/user_api/api/routes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from restalchemy.api import routes
1818

1919
from genesis_core.user_api.api import controllers
20+
from genesis_core.user_api.dns.api import routes as dns_routes
2021
from genesis_core.user_api.em.api import routes as em_routes
2122
from genesis_core.user_api.iam.api import routes as iam_routes
2223
from genesis_core.user_api.config.api import routes as config_routes
@@ -60,6 +61,7 @@ class ApiEndpointRoute(routes.Route):
6061
__controller__ = controllers.ApiEndpointController
6162
__allow_methods__ = [routes.FILTER]
6263

64+
dns = routes.route(dns_routes.DnsRoute)
6365
health = routes.route(HealthRoute)
6466
iam = routes.route(iam_routes.IamRoute)
6567
em = routes.route(em_routes.ElementManagerRoute)

genesis_core/user_api/dns/__init__.py

Whitespace-only changes.

genesis_core/user_api/dns/api/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)