CLI tool and library to migrate PowerDNS zones between servers
Project description
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.).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file powerdns_migrator-0.2.0.tar.gz.
File metadata
- Download URL: powerdns_migrator-0.2.0.tar.gz
- Upload date:
- Size: 18.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd7a6de1f903b00c789c62ec9fbb91bfc19c1f860d516dd86ebbbc8bc089910a
|
|
| MD5 |
bb502fbf9a5b724c0eed3ddcab89862e
|
|
| BLAKE2b-256 |
f0886c6d85175a61c8e8d421d2d09d586a2c423b554d53afb00e64102ac81ce6
|
File details
Details for the file powerdns_migrator-0.2.0-py3-none-any.whl.
File metadata
- Download URL: powerdns_migrator-0.2.0-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f0c7ca06d9a84e5128417f626c8f15b1769dc7ca8790038290c0e7550324a48
|
|
| MD5 |
6c8bbd540460fce6fc76295e321aa867
|
|
| BLAKE2b-256 |
219733792cc05b662d38397e14a37fc885cf5d8413aa6055dcbe843768e86988
|