Skip to main content

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 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.).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

powerdns_migrator-0.2.0.tar.gz (18.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

powerdns_migrator-0.2.0-py3-none-any.whl (17.7 kB view details)

Uploaded Python 3

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

Hashes for powerdns_migrator-0.2.0.tar.gz
Algorithm Hash digest
SHA256 bd7a6de1f903b00c789c62ec9fbb91bfc19c1f860d516dd86ebbbc8bc089910a
MD5 bb502fbf9a5b724c0eed3ddcab89862e
BLAKE2b-256 f0886c6d85175a61c8e8d421d2d09d586a2c423b554d53afb00e64102ac81ce6

See more details on using hashes here.

File details

Details for the file powerdns_migrator-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for powerdns_migrator-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1f0c7ca06d9a84e5128417f626c8f15b1769dc7ca8790038290c0e7550324a48
MD5 6c8bbd540460fce6fc76295e321aa867
BLAKE2b-256 219733792cc05b662d38397e14a37fc885cf5d8413aa6055dcbe843768e86988

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page