Files
ansible-bind9-role/scripts/compare_bind_versions.py
Daniel Akulenok dc4113088e feat: Add grammar fetch and comparison tooling
- Add fetch_bind_grammar.py for MCP-based grammar file retrieval
- Add compare_bind_versions.py for version differences analysis
- Add process_mcp_result.py for handling base64-encoded MCP output
- Create upstream directory structure with fetching instructions
- Document grammar file locations and structure
2026-02-07 22:52:35 +01:00

391 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Compare BIND9 grammar files between versions to identify breaking changes.
This script compares grammar files from two BIND9 versions and generates
a comprehensive report of:
- Removed options (breaking changes)
- Added options (new features)
- Modified option syntax
- Deprecated options
Usage:
python scripts/compare_bind_versions.py \\
--version1-dir bind9-grammar/upstream/v9.18.44 \\
--version2-dir bind9-grammar/upstream/v9.20.18 \\
--output docs/BIND_VERSION_DIFFERENCES.md
"""
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Set, Tuple
import re
class GrammarComparator:
"""Compare BIND9 grammar files between versions."""
def __init__(self, version1_dir: Path, version2_dir: Path):
"""
Initialize comparator with two version directories.
Args:
version1_dir: Path to first version's grammar files
version2_dir: Path to second version's grammar files
"""
self.version1_dir = version1_dir
self.version2_dir = version2_dir
self.version1_name = version1_dir.name
self.version2_name = version2_dir.name
def load_grammar_file(self, file_path: Path) -> Dict:
"""Load and parse a grammar file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Parse grammar file into structured data
options = self._parse_grammar(content)
return options
except FileNotFoundError:
return {}
def _parse_grammar(self, content: str) -> Dict[str, str]:
"""
Parse grammar content into a dictionary of options.
This is a simplified parser that extracts top-level keywords
and their definitions.
"""
options = {}
lines = content.split('\n')
for line in lines:
line = line.strip()
if not line or line.startswith('#') or line.startswith('//'):
continue
# Extract keyword and its definition
# Pattern: keyword <definition>; or keyword <definition> { ... };
match = re.match(r'^([a-z0-9-]+)\s+(.+?)(?:;|$)', line)
if match:
keyword = match.group(1)
definition = match.group(2).strip()
# Extract flags from comments
flags = []
if '// may occur multiple times' in line:
flags.append('may occur multiple times')
if '// deprecated' in line:
flags.append('deprecated')
if '// obsolete' in line:
flags.append('obsolete')
if '// not configured' in line:
flags.append('not configured')
if '// test only' in line:
flags.append('test only')
if '// experimental' in line:
flags.append('experimental')
options[keyword] = {
'definition': definition,
'flags': flags,
'raw_line': line
}
return options
def compare_files(self, filename: str) -> Dict:
"""
Compare a specific grammar file between two versions.
Returns:
Dict with added, removed, modified, and deprecated options
"""
file1 = self.version1_dir / 'grammar' / filename
file2 = self.version2_dir / 'grammar' / filename
options1 = self.load_grammar_file(file1)
options2 = self.load_grammar_file(file2)
keys1 = set(options1.keys())
keys2 = set(options2.keys())
# Identify changes
added = keys2 - keys1
removed = keys1 - keys2
common = keys1 & keys2
modified = []
deprecated_new = []
for key in common:
opt1 = options1[key]
opt2 = options2[key]
# Check if definition changed
if opt1['definition'] != opt2['definition']:
modified.append({
'option': key,
'old_definition': opt1['definition'],
'new_definition': opt2['definition']
})
# Check if newly deprecated
if 'deprecated' not in opt1['flags'] and 'deprecated' in opt2['flags']:
deprecated_new.append(key)
return {
'file': filename,
'added': sorted(added),
'removed': sorted(removed),
'modified': modified,
'deprecated_new': deprecated_new,
'options1_count': len(options1),
'options2_count': len(options2)
}
def compare_all(self) -> Dict:
"""Compare all grammar files between versions."""
# List of grammar files to compare
grammar_files = [
'options',
'forward.zoneopt',
'hint.zoneopt',
'in-view.zoneopt',
'mirror.zoneopt',
'primary.zoneopt',
'redirect.zoneopt',
'secondary.zoneopt',
'static-stub.zoneopt',
'stub.zoneopt',
'delegation-only.zoneopt',
'rndc.grammar',
]
results = {}
for filename in grammar_files:
result = self.compare_files(filename)
results[filename] = result
return results
def generate_markdown_report(self, results: Dict) -> str:
"""Generate a Markdown report from comparison results."""
lines = []
lines.append(f"# BIND9 Version Differences: {self.version1_name} vs {self.version2_name}")
lines.append("")
lines.append(f"This document compares BIND9 configuration grammar between {self.version1_name} and {self.version2_name}.")
lines.append("")
lines.append("Generated automatically by `scripts/compare_bind_versions.py`.")
lines.append("")
# Summary
lines.append("## Summary")
lines.append("")
total_added = sum(len(r['added']) for r in results.values())
total_removed = sum(len(r['removed']) for r in results.values())
total_modified = sum(len(r['modified']) for r in results.values())
total_deprecated = sum(len(r['deprecated_new']) for r in results.values())
lines.append(f"- **New Options**: {total_added}")
lines.append(f"- **Removed Options**: {total_removed} ⚠️")
lines.append(f"- **Modified Options**: {total_modified}")
lines.append(f"- **Newly Deprecated**: {total_deprecated}")
lines.append("")
# Breaking Changes
if total_removed > 0:
lines.append("## ⚠️ Breaking Changes")
lines.append("")
lines.append(f"The following options were removed in {self.version2_name} and will cause configuration errors:")
lines.append("")
for filename, result in results.items():
if result['removed']:
lines.append(f"### {filename}")
lines.append("")
for option in result['removed']:
lines.append(f"- `{option}`")
lines.append("")
# New Features
if total_added > 0:
lines.append("## ✨ New Features")
lines.append("")
lines.append(f"The following options were added in {self.version2_name}:")
lines.append("")
for filename, result in results.items():
if result['added']:
lines.append(f"### {filename}")
lines.append("")
for option in result['added']:
lines.append(f"- `{option}`")
lines.append("")
# Modified Options
if total_modified > 0:
lines.append("## 🔧 Modified Options")
lines.append("")
lines.append(f"The following options have syntax changes in {self.version2_name}:")
lines.append("")
for filename, result in results.items():
if result['modified']:
lines.append(f"### {filename}")
lines.append("")
for mod in result['modified']:
lines.append(f"#### `{mod['option']}`")
lines.append("")
lines.append(f"**{self.version1_name}**:")
lines.append(f"```")
lines.append(f"{mod['old_definition']}")
lines.append(f"```")
lines.append("")
lines.append(f"**{self.version2_name}**:")
lines.append(f"```")
lines.append(f"{mod['new_definition']}")
lines.append(f"```")
lines.append("")
# Deprecated Options
if total_deprecated > 0:
lines.append("## 📋 Newly Deprecated Options")
lines.append("")
lines.append(f"The following options were marked as deprecated in {self.version2_name}:")
lines.append("")
for filename, result in results.items():
if result['deprecated_new']:
lines.append(f"### {filename}")
lines.append("")
for option in result['deprecated_new']:
lines.append(f"- `{option}`")
lines.append("")
# File-by-File Comparison
lines.append("## Detailed File-by-File Comparison")
lines.append("")
for filename, result in results.items():
lines.append(f"### {filename}")
lines.append("")
lines.append(f"- {self.version1_name}: {result['options1_count']} options")
lines.append(f"- {self.version2_name}: {result['options2_count']} options")
lines.append(f"- Added: {len(result['added'])}")
lines.append(f"- Removed: {len(result['removed'])}")
lines.append(f"- Modified: {len(result['modified'])}")
lines.append("")
# Migration Guide
if total_removed > 0 or total_deprecated > 0:
lines.append("## Migration Guide")
lines.append("")
lines.append(f"### Migrating from {self.version1_name} to {self.version2_name}")
lines.append("")
if total_removed > 0:
lines.append("1. **Remove unsupported options** from your configuration")
lines.append(" - Review the Breaking Changes section above")
lines.append(" - Check if there are replacement options")
lines.append("")
if total_deprecated > 0:
lines.append("2. **Plan for deprecated options**")
lines.append(" - These options still work but may be removed in future versions")
lines.append(" - Start planning migration to recommended alternatives")
lines.append("")
lines.append("3. **Test your configuration**")
lines.append(" - Use `named-checkconf` to validate syntax")
lines.append(" - Test in a development environment before production")
lines.append("")
return '\n'.join(lines)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Compare BIND9 grammar files between versions"
)
parser.add_argument(
"--version1-dir",
type=Path,
required=True,
help="Directory containing first version's grammar files"
)
parser.add_argument(
"--version2-dir",
type=Path,
required=True,
help="Directory containing second version's grammar files"
)
parser.add_argument(
"--output",
type=Path,
default=Path("docs/BIND_VERSION_DIFFERENCES.md"),
help="Output file for Markdown report"
)
parser.add_argument(
"--json",
type=Path,
help="Also output raw comparison as JSON"
)
args = parser.parse_args()
# Validate directories
if not args.version1_dir.exists():
print(f"Error: {args.version1_dir} does not exist", file=sys.stderr)
sys.exit(1)
if not args.version2_dir.exists():
print(f"Error: {args.version2_dir} does not exist", file=sys.stderr)
sys.exit(1)
# Perform comparison
print(f"Comparing BIND9 versions:")
print(f" Version 1: {args.version1_dir.name}")
print(f" Version 2: {args.version2_dir.name}")
comparator = GrammarComparator(args.version1_dir, args.version2_dir)
results = comparator.compare_all()
# Generate and save Markdown report
report = comparator.generate_markdown_report(results)
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, 'w', encoding='utf-8') as f:
f.write(report)
print(f"✓ Markdown report saved to: {args.output}")
# Save JSON if requested
if args.json:
args.json.parent.mkdir(parents=True, exist_ok=True)
with open(args.json, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2)
print(f"✓ JSON comparison saved to: {args.json}")
# Print summary
total_added = sum(len(r['added']) for r in results.values())
total_removed = sum(len(r['removed']) for r in results.values())
total_modified = sum(len(r['modified']) for r in results.values())
print(f"\nComparison Summary:")
print(f" Added options: {total_added}")
print(f" Removed options: {total_removed}")
print(f" Modified options: {total_modified}")
if total_removed > 0:
print(f"\n⚠️ Warning: {total_removed} breaking changes detected!")
if __name__ == "__main__":
main()