Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F26465900
D4092.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
5 KB
Referenced Files
None
Subscribers
None
D4092.diff
View Options
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -52,3 +52,11 @@
hooks:
- id: shellcheck
args: ["--severity=warning"]
+
+ - repo: local
+ hooks:
+ - id: dns-soa-serial-validator
+ name: Validate DNS Zone SOA Serials
+ entry: scripts/precommit-dns-validator.sh
+ language: system
+ files: ^roles/dns/knot/files/zones/.*\.zone$
diff --git a/scripts/precommit-dns-validator.sh b/scripts/precommit-dns-validator.sh
new file mode 100755
--- /dev/null
+++ b/scripts/precommit-dns-validator.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+# Exit immediately if a fail happens
+set -e
+
+# Loop over all modified files passed by pre-commit
+for file in "$@"; do
+ case "$file" in
+ *.zone)
+ echo "Checking DNS zone: $file"
+
+ # Create a temporary file
+ old_file=$(mktemp)
+
+ # Get old version from Git
+ if git show "HEAD:$file" > "$old_file" 2>/dev/null; then
+ :
+ else
+ # New dns zone file
+ # Remove the tmp file
+ rm -f "$old_file"
+ # Skip to next file
+ continue
+ fi
+
+ # Call the Python validator
+ python3 scripts/validate_dns_serial.py "$old_file" "$file"
+
+ # Remove the tmp file
+ rm -f "$old_file"
+ ;;
+ esac
+done
diff --git a/scripts/validate_dns_serial.py b/scripts/validate_dns_serial.py
new file mode 100755
--- /dev/null
+++ b/scripts/validate_dns_serial.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+
+import re
+import sys
+from pathlib import Path
+
+
+# -------------------------------------------------------------
+# Configuration
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+# Match SOA serial number in a DNS zone file content (e.g. "2026041801 ;serial")
+SERIAL_REGEX = re.compile(r"(\d+)\s*;\s*serial", re.IGNORECASE)
+
+
+# -------------------------------------------------------------
+# Helper functions
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def read_file(path: str) -> str:
+ """
+ Read and return the content of a file.
+
+ Args:
+ path (str): Path to the file.
+
+ Returns:
+ str: Content of the file as a string.
+ """
+ return Path(path).read_text(encoding="utf-8")
+
+
+def extract_serial(content: str) -> int | None:
+ """
+ Extract the SOA serial number from a DNS zone file content.
+
+ Args:
+ content (str): Content of the DNS zone file.
+
+ Returns:
+ int | None: The serial number if found, otherwise None.
+ """
+ match = SERIAL_REGEX.search(content)
+ if not match:
+ return None
+ return int(match.group(1))
+
+
+def normalize_zone(content: str) -> str:
+ """
+ Remove the serial and clean the text (e.g. remove extra spaces)
+ to properly compare two DNS zone files.
+
+ Args:
+ content (str): Content of the DNS zone file.
+
+ Returns:
+ str: Normalized content of the DNS zone file ready for comparison.
+ """
+ content = SERIAL_REGEX.sub("SERIAL ; serial", content)
+
+ # 1. splitlines → split content into lines
+ # 2. for line → iterate over each line
+ # 3. split() → check if line is not empty (if line.split())
+ # 4. split() → split line into words (remove all extra spaces)
+ # 5. join " " → rebuild each line with a single space
+ # 6. join "\n" → rebuild full text with newlines
+ # 7. strip → remove leading and trailing space
+ return "\n".join(
+ " ".join(line.split()) for line in content.splitlines() if line.split()
+ ).strip()
+
+
+# -------------------------------------------------------------
+# Main function
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def main() -> int:
+ if len(sys.argv) != 3:
+ print(f"Usage: {sys.argv[0]} <old_zone_file> <new_zone_file>")
+ return 2
+
+ old_file = sys.argv[1]
+ new_file = sys.argv[2]
+
+ old_content = read_file(old_file)
+ new_content = read_file(new_file)
+
+ old_serial = extract_serial(old_content)
+ new_serial = extract_serial(new_content)
+
+ if new_serial is None:
+ print(f"ERROR: Could not find SOA serial in new file: {new_file}")
+ return 1
+
+ old_zone_normalized = normalize_zone(old_content)
+ new_zone_normalized = normalize_zone(new_content)
+
+ if old_zone_normalized == new_zone_normalized:
+ print(f"OK: No DNS record change detected in {new_file}")
+ return 0
+
+ if new_serial <= old_serial:
+ print(
+ f"ERROR: DNS zone changed but SOA serial was not bumped "
+ f"({old_serial} -> {new_serial}) in {new_file}"
+ )
+ return 1
+
+ print(
+ f"OK: DNS zone changed and SOA serial was bumped ({old_serial} -> {new_serial})"
+ )
+ return 0
+
+
+# -------------------------------------------------------------
+# pre-commit hook - Entry point
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+if __name__ == "__main__":
+ sys.exit(main())
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 24, 02:20 (22 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3653769
Default Alt Text
D4092.diff (5 KB)
Attached To
Mode
D4092: Add DNS SOA serial validation pre-commit hook
Attached
Detach File
Event Timeline
Log In to Comment