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-migrate \
  --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-migrate \
  --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

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'])}")
    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)

Example output:

# Test single zone migration
powerdns-migrate \
  --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. \
  --dry-run

# Test batch migration
powerdns-migrate \
  --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 10 \
  --dry-run

The migrator will report what actions would be taken (CREATE_ZONE, PATCH_ZONE, RECREATE_ZONE, or NOOP) and provide detailed change information for each zone.

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.1.0.tar.gz (17.7 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.1.0-py3-none-any.whl (17.1 kB view details)

Uploaded Python 3

File details

Details for the file powerdns_migrator-0.1.0.tar.gz.

File metadata

  • Download URL: powerdns_migrator-0.1.0.tar.gz
  • Upload date:
  • Size: 17.7 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.1.0.tar.gz
Algorithm Hash digest
SHA256 80419ecfa5b300a113f74c8288887fe43c2b3f44961aba57aff0ebf98454bde0
MD5 1f4bb5982a807509e096519bf7f4d96c
BLAKE2b-256 72b2a9a08608bf6113d142388791126901321cefa1dd970f4afb1554cf0b5223

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for powerdns_migrator-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9d1888ee533894f8ca8c39e7af2fa1076795c2bdaeafeb6e558cdda939f3f4ed
MD5 000be144fc26fdd69814e3e034910711
BLAKE2b-256 bd6056142019b78743bd0ee820da07db8f69d72fc7c49c42869e5bd5e7362b0b

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