powerdns-migrator
Python package and CLI to migrate DNS zones between PowerDNS servers using their HTTP API.
Features
- CLI & Library — Use as a command-line tool or import directly into your Python projects
- Async & Parallel — Batch migrate thousands of zones concurrently with configurable parallelism
- Smart Sync — Detects differences and merges only changed records, or fully recreates zones
- Dry-Run Mode — Preview changes before applying them to the target server
- Auto-Fix CNAME Conflicts — Automatically resolve CNAME/other record conflicts at the same name
- Retry with Backoff — Configurable retries with exponential backoff and jitter for transient errors
- TXT Escape Normalization — Handle different TXT record encodings across backends (MySQL, etc.)
- Error Handling — Choose to stop or continue on errors during batch migrations
- Progress Reporting — Real-time progress logs for large batch migrations
Install
pip install powerdns-migrator
Usage
powerdns-migrator \
--source-url https://pdns-source:8081 \
--source-key "$PDNS_SOURCE_KEY" \
--target-url https://pdns-target:8081 \
--target-key "$PDNS_TARGET_KEY" \
--zone example.com. \
--recreate
Batch mode (async, parallel):
powerdns-migrator \
--source-url https://pdns-source:8081 \
--source-key "$PDNS_SOURCE_KEY" \
--target-url https://pdns-target:8081 \
--target-key "$PDNS_TARGET_KEY" \
--zones-file /path/to/zones.txt \
--concurrency 5
Key flags:
--server-id: PowerDNS server id (default:localhost)--recreate: delete and recreate target zone if it already exists (without this flag, changes are merged)--dry-run: fetch and validate zones without making changes to target server--insecure-source/--insecure-target: skip TLS verification for each side--timeout: HTTP timeout in seconds (default: 10)--retries: retry count for transient API errors--retry-backoff: base backoff seconds between retries--retry-max-backoff: maximum backoff seconds between retries--retry-jitter: max random jitter seconds added to backoff--ignore-soa-serial: ignore SOA serial changes and keep target serial--auto-fix-cname-conflicts: auto-fix CNAME conflicts (drop other types on same name, but drop CNAME at apex)--auto-fix-double-cname-conflicts: trim multi-record CNAME rrsets to a single record (first one wins)--normalize-txt-escapes: normalize TXT/SPF decimal escape sequences (e.g.\239) to raw bytes for comparison--on-error: batch behavior on API error (continue or stop)--zones-file: migrate zones from a file (one per line)--concurrency: parallel migrations when using--zones-file--graceful-timeout: stop after N seconds on Ctrl+C (0 = wait indefinitely)--progress-interval: progress log interval in seconds (0 = disable)--log-level: set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)--verbose: enable debug logging (alias for--log-level DEBUG)
Library use
import asyncio
from powerdns_migrator.async_migrator import AsyncZoneMigrator
from powerdns_migrator.config import PowerDNSConnection
from powerdns_migrator.errors import PowerDNSAPIError, PowerDNSConnectionError, PowerDNSMigratorError
source = PowerDNSConnection(
base_url="https://pdns-source:8081",
api_key="SOURCE_KEY",
)
target = PowerDNSConnection(
base_url="https://pdns-target:8081",
api_key="TARGET_KEY",
)
async def run():
migrator = AsyncZoneMigrator(source, target)
try:
result = await migrator.migrate("example.com.", recreate=True, dry_run=False)
print(f"Migration completed: {result['migrator_action']}")
print(f"Changes applied: {len(result['changes'])}")
except PowerDNSAPIError as exc:
print(f"API error: {exc.status} {exc.body}")
except PowerDNSConnectionError as exc:
print(f"Connection error: {exc.cause}")
except PowerDNSMigratorError as exc:
print(f"Migration error: {exc}")
finally:
await migrator.close()
asyncio.run(run())
PowerDNSConnection Arguments
| Argument | Type | Default | Description |
|---|---|---|---|
base_url |
str |
required | PowerDNS API base URL (e.g. https://pdns:8081) |
api_key |
str |
required | PowerDNS API key |
server_id |
str |
"localhost" |
PowerDNS server id |
verify_ssl |
bool |
True |
Verify TLS certificates |
AsyncZoneMigrator Arguments
| Argument | Type | Default | Description |
|---|---|---|---|
source |
PowerDNSConnection |
required | Source PowerDNS connection config |
target |
PowerDNSConnection |
required | Target PowerDNS connection config |
timeout |
float |
10.0 |
HTTP timeout in seconds |
retries |
int |
3 |
Retry count for transient API errors |
retry_backoff |
float |
0.5 |
Base backoff seconds between retries |
retry_max_backoff |
float |
5.0 |
Maximum backoff seconds between retries |
retry_jitter |
float |
0.1 |
Max random jitter seconds added to backoff |
ignore_soa_serial |
bool |
False |
Ignore SOA serial changes and keep target serial |
auto_fix_cname_conflicts |
bool |
False |
Auto-fix CNAME conflicts (drop other types on same name, but drop CNAME at apex) |
auto_fix_double_cname_conflicts |
bool |
False |
Trim multi-record CNAME rrsets to single record (first one wins) |
normalize_txt_escapes |
bool |
False |
Normalize TXT/SPF decimal escape sequences to raw bytes for comparison |
migrate() Arguments
| Argument | Type | Default | Description |
|---|---|---|---|
zone_name |
str |
required | Zone name to migrate (e.g. "example.com.") |
recreate |
bool |
False |
Delete and recreate target zone if it exists (otherwise changes are merged) |
dry_run |
bool |
False |
Validate and compute changes without modifying target |
Migration Result Structure
The migrate() method returns a dictionary with detailed information about the migration:
{
"source_zone": {...}, # Sanitized zone data from source
"target_zone": {...}, # Zone data from target (empty in dry-run mode)
"changes": {...}, # RRSet changes that were/would be applied
"migrator_action": "..." # Action taken: CREATE_ZONE, PATCH_ZONE, RECREATE_ZONE, or NOOP
}
Action Types:
CREATE_ZONE: Zone created on target (didn't exist before)PATCH_ZONE: Zone updated with specific RRSet changesRECREATE_ZONE: Zone deleted and recreated (when using--recreate)NOOP: No changes needed (zone already in sync)
Dry Run Mode
The --dry-run flag allows you to test migrations safely without making any changes to the target PowerDNS server. This is useful for:
- Validation: Verify zones can be fetched and parsed from source
- Change Analysis: See exactly what would be migrated or modified
- Safety: Test configurations and permissions before real migrations
What dry-run does:
- ✅ Fetches zone data from source PowerDNS server
- ✅ Sanitizes and validates zone structure
- ✅ Computes required changes on target server
- ✅ Returns detailed migration plan and statistics
- ❌ Does NOT create, delete, or modify zones on target server
- ❌ Does NOT make any API calls to target server (except zone existence checks)
Examples
The repository includes ready-to-use examples for common integration patterns in the examples/ directory.
Notes
- This packages is under active development. It intentionally keeps behavior simple: it fetches the entire zone (including rrsets) from the source and recreates it on the target.
- The migrator drops read-only fields returned by PowerDNS (
id,url,serial,notified_serial, etc.). - For existing zones on the target, use
--recreateto delete before recreate. - When
--auto-fix-cname-conflictsis enabled, apex CNAMEs are removed and non-apex CNAMEs are kept while other rrsets with the same name are dropped. - When
--auto-fix-double-cname-conflictsis enabled, multi-record CNAME rrsets are trimmed to the first record. - When
--normalize-txt-escapesis enabled, TXT/SPF records with decimal escape sequences (e.g.\239\191\189) are normalized to raw bytes during comparison. This is useful when migrating between backends that represent non-ASCII content differently (e.g. MySQL vs LMDB). - Tested with PowerDNS API v1. Additional adjustments may be needed for specific setups (DNSSEC, presigned zones, custom backends, etc.).
Metadata
Release files for powerdns-migrator 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| powerdns_migrator-0.2.0.tar.gz | 18.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| powerdns_migrator-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 35.9 kB
Release files / powerdns_migrator-0.2.0.tar.gz
| Download URL | powerdns_migrator-0.2.0.tar.gz |
|---|---|
| Size | 18.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd7a6de1f903b00c789c62ec9fbb91bfc19c1f860d516dd86ebbbc8bc089910a
|
|
BLAKE2b-256 checksum How to use checksums |
f0886c6d85175a61c8e8d421d2d09d586a2c423b554d53afb00e64102ac81ce6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / powerdns_migrator-0.2.0-py3-none-any.whl
| Download URL | powerdns_migrator-0.2.0-py3-none-any.whl |
|---|---|
| Size | 17.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1f0c7ca06d9a84e5128417f626c8f15b1769dc7ca8790038290c0e7550324a48
|
|
BLAKE2b-256 checksum How to use checksums |
219733792cc05b662d38397e14a37fc885cf5d8413aa6055dcbe843768e86988
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|