Skip to main content

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 changes
  • RECREATE_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 --recreate to delete before recreate.
  • When --auto-fix-cname-conflicts is 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-conflicts is enabled, multi-record CNAME rrsets are trimmed to the first record.
  • When --normalize-txt-escapes is 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)

Source distribution for powerdns-migrator 0.2.0
File Size Uploaded
powerdns_migrator-0.2.0.tar.gz 18.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for powerdns-migrator 0.2.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page