summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/docs_publish.yml2
-rw-r--r--.github/workflows/docs_source_update.yml2
-rw-r--r--.github/workflows/formatting_check.yml2
-rw-r--r--.github/workflows/linting_check.yml2
-rw-r--r--.github/workflows/pypi-publish-release.yml2
-rw-r--r--.github/workflows/unit_tests.yml2
-rw-r--r--pkb_client/__init__.py2
-rw-r--r--pkb_client/client/bind_file.py45
-rw-r--r--pkb_client/client/client.py64
-rw-r--r--pkb_client/client/dns.py43
-rw-r--r--requirements.txt2
-rw-r--r--tests/bind_file.py52
-rw-r--r--tests/client.py16
-rw-r--r--tests/data/test.bind12
-rw-r--r--tests/data/test_no_ttl.bind12
15 files changed, 194 insertions, 66 deletions
diff --git a/.github/workflows/docs_publish.yml b/.github/workflows/docs_publish.yml
index 9b6a687..719e23a 100644
--- a/.github/workflows/docs_publish.yml
+++ b/.github/workflows/docs_publish.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python 3.12
- uses: actions/setup-python@v5.5.0
+ uses: actions/setup-python@v5.6.0
with:
python-version: 3.12
diff --git a/.github/workflows/docs_source_update.yml b/.github/workflows/docs_source_update.yml
index f90a4c3..443943a 100644
--- a/.github/workflows/docs_source_update.yml
+++ b/.github/workflows/docs_source_update.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python 3.12
- uses: actions/setup-python@v5.5.0
+ uses: actions/setup-python@v5.6.0
with:
python-version: 3.12
diff --git a/.github/workflows/formatting_check.yml b/.github/workflows/formatting_check.yml
index 7501ad0..f48cf74 100644
--- a/.github/workflows/formatting_check.yml
+++ b/.github/workflows/formatting_check.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5.5.0
+ uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
diff --git a/.github/workflows/linting_check.yml b/.github/workflows/linting_check.yml
index 113da7f..d61d032 100644
--- a/.github/workflows/linting_check.yml
+++ b/.github/workflows/linting_check.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5.5.0
+ uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
diff --git a/.github/workflows/pypi-publish-release.yml b/.github/workflows/pypi-publish-release.yml
index e89dcd3..692897a 100644
--- a/.github/workflows/pypi-publish-release.yml
+++ b/.github/workflows/pypi-publish-release.yml
@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python 3.9
- uses: actions/setup-python@v5.5.0
+ uses: actions/setup-python@v5.6.0
with:
python-version: 3.9
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 32899c2..66dc638 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5.5.0
+ uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
diff --git a/pkb_client/__init__.py b/pkb_client/__init__.py
index 93d9382..6cd5d0d 100644
--- a/pkb_client/__init__.py
+++ b/pkb_client/__init__.py
@@ -1 +1 @@
-__version__ = "v2.1.1"
+__version__ = "v2.2.0"
diff --git a/pkb_client/client/bind_file.py b/pkb_client/client/bind_file.py
index af9abe0..17bb5ee 100644
--- a/pkb_client/client/bind_file.py
+++ b/pkb_client/client/bind_file.py
@@ -27,7 +27,7 @@ class BindRecord:
record_string = f"{self.name} {self.ttl} {self.record_class} {self.record_type}"
if self.prio is not None:
record_string += f" {self.prio}"
- record_string += f" {self.data}"
+ record_string += f' "{self.data}"'
if self.comment:
record_string += f" ; {self.comment}"
return record_string
@@ -68,18 +68,14 @@ class BindFile:
# 2: name record-class ttl record-type record-data
# whereby the ttl is optional
- # drop any right trailing comments
- line_parts = line.split(";", 1)
- line = line_parts[0].strip()
- comment = line_parts[1].strip() if len(line_parts) > 1 else None
- prio = None
+ record_parts = line.strip().split()
- # skip empty lines
- if not line:
+ # skip comments
+ if not record_parts or record_parts[0].startswith(";"):
continue
- # find which format the line is
- record_parts = line.split()
+ prio = None
+
if record_parts[1].isdigit():
# scheme 1
if record_parts[3] not in DNSRecordType.__members__:
@@ -137,6 +133,35 @@ class BindFile:
# replace @ in record name with origin
record_name = record_name.replace("@", origin)
+ # handle comments and quoted strings as record data
+ comment = None
+ line = record_data.strip()
+ if line.startswith('"'):
+ # find rightmost double quote
+ rindex = line.rfind('"')
+ if rindex != -1:
+ # split at the last double quote
+ line_parts = line.rsplit('"', 1)
+ record_data = line_parts[0].strip('"')
+
+ comment = line_parts[1].strip() if len(line_parts) > 1 else None
+ # left strip semicolon from comment
+ if comment and comment.startswith(";"):
+ comment = comment[1:].strip()
+
+ if not comment:
+ comment = None
+ else:
+ record_data = line.strip('"')
+ else:
+ # try to split at the first semicolon for comments
+ if ";" in line:
+ record_data, comment = line.split(";", 1)
+ record_data = record_data.strip()
+ comment = comment.strip()
+ else:
+ record_data = line
+
records.append(
BindRecord(
record_name,
diff --git a/pkb_client/client/client.py b/pkb_client/client/client.py
index 81b946b..469751e 100644
--- a/pkb_client/client/client.py
+++ b/pkb_client/client/client.py
@@ -128,7 +128,7 @@ class PKBClient:
req_json = {
**self._get_auth_request_json(),
"name": name,
- "type": record_type,
+ "type": record_type.value,
"content": content,
"ttl": ttl,
"prio": prio,
@@ -182,7 +182,7 @@ class PKBClient:
req_json = {
**self._get_auth_request_json(),
"name": name,
- "type": record_type,
+ "type": record_type.value,
"content": content,
"ttl": ttl,
"prio": prio,
@@ -234,7 +234,7 @@ class PKBClient:
)
req_json = {
**self._get_auth_request_json(),
- "type": record_type,
+ "type": record_type.value,
"content": content,
"ttl": ttl,
"prio": prio,
@@ -430,7 +430,7 @@ class PKBClient:
logger.warning("file already exists, overwriting...")
# domain header
- bind_file_content = f"$ORIGIN {domain}"
+ bind_file_content = f"$ORIGIN {domain}."
# SOA record
soa_records = dns.resolver.resolve(domain, "SOA")
@@ -441,7 +441,15 @@ class PKBClient:
# records
for record in dns_records:
# name record class ttl record type record data
- if record.prio:
+ # add trailing dot to the name if it is a supported record type, to make it a fully qualified domain name
+ if record.type in [
+ DNSRecordType.MX,
+ DNSRecordType.CNAME,
+ DNSRecordType.NS,
+ DNSRecordType.SRV,
+ ]:
+ record.content += "."
+ if record.prio is not None:
record_content = f"{record.prio} {record.content}"
else:
record_content = record.content
@@ -498,7 +506,7 @@ class PKBClient:
name = ".".join(exported_record["name"].split(".")[:-2])
self.create_dns_record(
domain=domain,
- record_type=exported_record["type"],
+ record_type=DNSRecordType(exported_record["type"]),
content=exported_record["content"],
name=name,
ttl=exported_record["ttl"],
@@ -534,7 +542,7 @@ class PKBClient:
self.update_dns_record(
domain=domain,
record_id=existing_record.id,
- record_type=record["type"],
+ record_type=DNSRecordType(record["type"]),
content=record["content"],
name=record["name"].replace(f".{domain}", ""),
ttl=record["ttl"],
@@ -564,7 +572,7 @@ class PKBClient:
if existing_record is None:
self.create_dns_record(
domain=domain,
- record_type=record["type"],
+ record_type=DNSRecordType(record["type"]),
content=record["content"],
name=record["name"].replace(f".{domain}", ""),
ttl=record["ttl"],
@@ -607,12 +615,20 @@ class PKBClient:
for record in existing_dns_records:
self.delete_dns_record(bind_file.origin[:-1], record.id)
+ nameserver_records = []
# restore all records from BIND file by creating new DNS records
for record in bind_file.records:
- # extract subdomain from record name
- subdomain = record.name.replace(bind_file.origin, "")
- # replace trailing dot
- subdomain = subdomain[:-1] if subdomain.endswith(".") else subdomain
+ if record.record_type == DNSRecordType.NS:
+ # collect nameserver records to update them later in bulk
+ nameserver_records.append(record)
+ continue
+ if record.name.endswith("."):
+ # extract subdomain from record name, by removing the domain and TLD
+ subdomain = record.name.removesuffix(bind_file.origin)
+ subdomain = subdomain.removesuffix(".")
+ else:
+ subdomain = record.name
+
self.create_dns_record(
domain=bind_file.origin[:-1],
record_type=record.record_type,
@@ -622,6 +638,17 @@ class PKBClient:
prio=record.prio,
)
+ # update nameservers in bulk
+ if nameserver_records:
+ name_servers = []
+ # remove trailing dot from nameserver records
+ for nameserver in nameserver_records:
+ if nameserver.data.endswith("."):
+ name_servers.append(nameserver.data[:-1])
+ else:
+ name_servers.append(nameserver.data)
+ self.update_dns_servers(bind_file.origin[:-1], name_servers)
+
except Exception as e:
logger.error("something went wrong: {}".format(e.__str__()))
self.__handle_error_backup__(existing_dns_records)
@@ -755,7 +782,7 @@ class PKBClient:
**self._get_auth_request_json(),
"subdomain": subdomain,
"location": location,
- "type": type,
+ "type": type.value,
"includePath": include_path,
"wildcard": wildcard,
}
@@ -950,11 +977,18 @@ class PKBClient:
)
@staticmethod
- def __handle_error_backup__(dns_records):
+ def __handle_error_backup__(dns_records: list[DNSRecord]) -> None:
+ """
+ Handle errors when working with dns records by creating a backup of the given DNS records.
+ Crates a backup file in the current working directory with an incremental suffix.
+
+ :param dns_records: the DNS records to backup
+ """
+
# merge the single DNS records into one single dict with the record id as key
dns_records_dict = dict()
for record in dns_records:
- dns_records_dict[record["id"]] = record
+ dns_records_dict[record.id] = record.to_dict()
# generate filename with incremental suffix
base_backup_filename = "pkb_client_dns_records_backup"
diff --git a/pkb_client/client/dns.py b/pkb_client/client/dns.py
index 85ae971..8bedad2 100644
--- a/pkb_client/client/dns.py
+++ b/pkb_client/client/dns.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from enum import Enum
-from typing import Optional
+from typing import Optional, Any
class DNSRecordType(str, Enum):
@@ -33,7 +33,14 @@ class DNSRecord:
notes: str
@staticmethod
- def from_dict(d):
+ def from_dict(d: dict[str, Any]) -> "DNSRecord":
+ """
+ Create a DNSRecord instance from a dictionary representation.
+
+ :param d: Dictionary containing DNS record data.
+ :return: DNSRecord instance.
+ """
+
# only use prio for supported record types since the API returns it for all records with default value 0
prio = int(d["prio"]) if d["type"] in DNS_RECORDS_WITH_PRIORITY else None
return DNSRecord(
@@ -46,6 +53,23 @@ class DNSRecord:
notes=d["notes"],
)
+ def to_dict(self) -> dict[str, Any]:
+ """
+ Convert the DNSRecord instance to a dictionary representation.
+
+ :return: Dictionary containing DNS record data.
+ """
+
+ return {
+ "id": self.id,
+ "name": self.name,
+ "type": str(self.type),
+ "content": self.content,
+ "ttl": self.ttl,
+ "prio": self.prio,
+ "notes": self.notes,
+ }
+
class DNSRestoreMode(Enum):
clear = 0
@@ -56,8 +80,13 @@ class DNSRestoreMode(Enum):
return self.name
@staticmethod
- def from_string(a):
- try:
- return DNSRestoreMode[a]
- except KeyError:
- return a
+ def from_string(a: str) -> "DNSRestoreMode":
+ """
+ Convert a string to a DNSRestoreMode enum member.
+
+ :param a: String representation of the restore mode.
+ :return: Corresponding DNSRestoreMode enum member.
+ :raises KeyError: If the string does not match any enum member.
+ """
+
+ return DNSRestoreMode[a]
diff --git a/requirements.txt b/requirements.txt
index e129f40..c9a1ccc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,4 @@ requests>=2.20.0
sphinx~=7.4
dnspython~=2.7
responses~=0.25.7
-ruff~=0.11
+ruff~=0.12
diff --git a/tests/bind_file.py b/tests/bind_file.py
index 4d1114f..7062146 100644
--- a/tests/bind_file.py
+++ b/tests/bind_file.py
@@ -15,7 +15,7 @@ class TestBindFileParsing(unittest.TestCase):
self.assertEqual("test.com.", bind_file.origin)
self.assertEqual(1234, bind_file.ttl)
- self.assertEqual(5, len(bind_file.records))
+ self.assertEqual(7, len(bind_file.records))
self.assertEqual(
BindRecord(
"test.com.", 600, RecordClass.IN, DNSRecordType.A, "1.2.3.4"
@@ -24,12 +24,23 @@ class TestBindFileParsing(unittest.TestCase):
)
self.assertEqual(
BindRecord(
- "sub.test.com.", 600, RecordClass.IN, DNSRecordType.A, "4.3.2.1"
+ "test.com.",
+ 600,
+ RecordClass.IN,
+ DNSRecordType.A,
+ "1.2.3.5",
+ comment="This is a comment",
),
bind_file.records[1],
)
self.assertEqual(
BindRecord(
+ "sub.test.com.", 600, RecordClass.IN, DNSRecordType.A, "4.3.2.1"
+ ),
+ bind_file.records[2],
+ )
+ self.assertEqual(
+ BindRecord(
"test.com.",
600,
RecordClass.IN,
@@ -37,13 +48,13 @@ class TestBindFileParsing(unittest.TestCase):
"2001:db8::1",
comment="This is a comment",
),
- bind_file.records[2],
+ bind_file.records[3],
)
self.assertEqual(
BindRecord(
"test.com.", 1234, RecordClass.IN, DNSRecordType.TXT, "pkb-client"
),
- bind_file.records[3],
+ bind_file.records[4],
)
self.assertEqual(
BindRecord(
@@ -54,7 +65,7 @@ class TestBindFileParsing(unittest.TestCase):
"mail.test.com.",
prio=10,
),
- bind_file.records[4],
+ bind_file.records[5],
)
with self.subTest("Without default TTL"):
@@ -63,7 +74,7 @@ class TestBindFileParsing(unittest.TestCase):
self.assertEqual("test.com.", bind_file.origin)
self.assertEqual(None, bind_file.ttl)
- self.assertEqual(5, len(bind_file.records))
+ self.assertEqual(7, len(bind_file.records))
self.assertEqual(
BindRecord(
"test.com.", 600, RecordClass.IN, DNSRecordType.A, "1.2.3.4"
@@ -72,12 +83,23 @@ class TestBindFileParsing(unittest.TestCase):
)
self.assertEqual(
BindRecord(
- "sub.test.com.", 600, RecordClass.IN, DNSRecordType.A, "4.3.2.1"
+ "test.com.",
+ 600,
+ RecordClass.IN,
+ DNSRecordType.A,
+ "1.2.3.5",
+ comment="This is a comment",
),
bind_file.records[1],
)
self.assertEqual(
BindRecord(
+ "sub.test.com.", 600, RecordClass.IN, DNSRecordType.A, "4.3.2.1"
+ ),
+ bind_file.records[2],
+ )
+ self.assertEqual(
+ BindRecord(
"test.com.",
700,
RecordClass.IN,
@@ -85,13 +107,13 @@ class TestBindFileParsing(unittest.TestCase):
"2001:db8::1",
comment="This is a comment",
),
- bind_file.records[2],
+ bind_file.records[3],
)
self.assertEqual(
BindRecord(
"test.com.", 700, RecordClass.IN, DNSRecordType.TXT, "pkb-client"
),
- bind_file.records[3],
+ bind_file.records[4],
)
self.assertEqual(
BindRecord(
@@ -102,7 +124,7 @@ class TestBindFileParsing(unittest.TestCase):
"mail.test.com.",
prio=10,
),
- bind_file.records[4],
+ bind_file.records[5],
)
def test_writing_bind_file(self):
@@ -131,11 +153,11 @@ class TestBindFileParsing(unittest.TestCase):
file_content = (
"$ORIGIN test.com.\n"
"$TTL 1234\n"
- "test.com. 600 IN A 1.2.3.4\n"
- "sub.test.com. 700 IN A 4.3.2.1\n"
- "test.com. 600 IN AAAA 2001:db8::1\n"
- "test.com. 600 IN TXT pkb-client\n"
- "test.com. 600 IN MX 10 mail.test.com.\n"
+ 'test.com. 600 IN A "1.2.3.4"\n'
+ 'sub.test.com. 700 IN A "4.3.2.1"\n'
+ 'test.com. 600 IN AAAA "2001:db8::1"\n'
+ 'test.com. 600 IN TXT "pkb-client"\n'
+ 'test.com. 600 IN MX 10 "mail.test.com."\n'
)
with tempfile.NamedTemporaryFile() as f:
diff --git a/tests/client.py b/tests/client.py
index 3b3a813..719ab84 100644
--- a/tests/client.py
+++ b/tests/client.py
@@ -1005,6 +1005,19 @@ class TestClientAuth(unittest.TestCase):
)
],
)
+ responses.post(
+ url=urljoin(API_ENDPOINT, "domain/updateNs/example.com"),
+ json={"status": "SUCCESS"},
+ match=[
+ matchers.json_params_matcher(
+ {
+ "apikey": "key",
+ "secretapikey": "secret",
+ "ns": ["ns1.example.com"],
+ }
+ )
+ ],
+ )
with tempfile.TemporaryDirectory() as temp_dir:
filename = Path(temp_dir, "records.bind")
@@ -1015,7 +1028,8 @@ class TestClientAuth(unittest.TestCase):
"$TTL 1234\n"
"@ IN SOA dns.example.com. dns2.example.com. (100 300 100 6000 600)\n"
"example.com. IN 600 A 127.0.0.3\n"
- "sub.example.com. 600 IN A 127.0.0.4"
+ "sub.example.com. 600 IN A 127.0.0.4\n"
+ "example.com IN 86400 NS ns1.example.com."
)
)
diff --git a/tests/data/test.bind b/tests/data/test.bind
index b99dcf4..4f04129 100644
--- a/tests/data/test.bind
+++ b/tests/data/test.bind
@@ -2,9 +2,11 @@ $ORIGIN test.com.
$TTL 1234
@ IN SOA dns.example.com. dns2.example.com. (100 300 100 6000 600)
test.com. IN 600 A 1.2.3.4
-test.com. HS 600 A 1.2.3.4
-sub.test.com. 600 IN A 4.3.2.1
-@ IN 600 AAAA 2001:db8::1 ; This is a comment
+test.com. IN 600 A 1.2.3.5 ; This is a comment
+test.com. HS 600 A "1.2.3.4"
+sub.test.com. 600 IN A "4.3.2.1"
+@ IN 600 AAAA "2001:db8::1" ; This is a comment
-test.com. IN TXT pkb-client
-test.com. 600 IN MX 10 mail.test.com.
+test.com. IN TXT "pkb-client"
+test.com. 600 IN MX 10 "mail.test.com."
+test.com. 600 IN TXT "test;test2" ; This is a comment
diff --git a/tests/data/test_no_ttl.bind b/tests/data/test_no_ttl.bind
index 7dac0ff..52e9b0f 100644
--- a/tests/data/test_no_ttl.bind
+++ b/tests/data/test_no_ttl.bind
@@ -1,9 +1,11 @@
$ORIGIN test.com.
@ IN SOA dns.example.com. dns2.example.com. (100 300 100 6000 600)
test.com. IN 600 A 1.2.3.4
-test.com. HS 600 A 1.2.3.4
-sub.test.com. 600 IN A 4.3.2.1
-@ IN 700 AAAA 2001:db8::1 ; This is a comment
+test.com. IN 600 A 1.2.3.5 ; This is a comment
+test.com. HS 600 A "1.2.3.4"
+sub.test.com. 600 IN A "4.3.2.1"
+@ IN 700 AAAA "2001:db8::1" ; This is a comment
-test.com. IN TXT pkb-client
-test.com. 600 IN MX 10 mail.test.com.
+test.com. IN TXT "pkb-client"
+test.com. 600 IN MX 10 "mail.test.com."
+test.com. 600 IN TXT "test;test2" ; This is a comment