From e2877ff2938bd939b87153773e0671b8249aeba9 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Thu, 25 Sep 2025 15:44:43 +0200 Subject: [PATCH] Add a pdnsutil "zone copy" command, and suggest its use for views. Fixes: #5798 Signed-off-by: Miod Vallat --- docs/manpages/pdnsutil.1.rst | 6 + docs/views.rst | 6 +- pdns/pdnsutil.cc | 217 +++++++++++++++++++++++------------ 3 files changed, 156 insertions(+), 73 deletions(-) diff --git a/docs/manpages/pdnsutil.1.rst b/docs/manpages/pdnsutil.1.rst index 101604a97..deeec130b 100644 --- a/docs/manpages/pdnsutil.1.rst +++ b/docs/manpages/pdnsutil.1.rst @@ -191,6 +191,12 @@ zone clear *ZONE* Clear the records in zone *ZONE*, but leave actual zone and settings unchanged +zone copy *ZONE* *NEW-ZONE* + + Copies the contents of *ZONE* (records, comments, metadata, keys) to a + new zone *NEW-ZONE*. The new zone must not exist and gets created as + part of the copy, in the same backend as *ZONE*. + zone create *ZONE* Create an empty zone named *ZONE*. diff --git a/docs/views.rst b/docs/views.rst index 3fb83ef5f..55d8b2287 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -166,7 +166,11 @@ to create these zones, like you would do for any other "regular" zone:: pdnsutil zone create example.com..trusted and then use `zone load`, `zone edit`, or `rrset add` to add contents to these -zones. +zones; or you may copy the contents of an existing zone:: + + pdnsutil zone copy example.com..internal example.com..trusted + +and then use `zone edit` to adjust the contents as needed. With these settings in place, queries for the `example.com.` zone will be performed on the `example.com..internal` zone when originating from the internal diff --git a/pdns/pdnsutil.cc b/pdns/pdnsutil.cc index 04e7b8b7a..4721fe441 100644 --- a/pdns/pdnsutil.cc +++ b/pdns/pdnsutil.cc @@ -88,6 +88,7 @@ static int changeSecondaryZonePrimary(vector& cmds, std::string_view syn static int checkAllZones(vector& cmds, std::string_view synopsis); static int checkZone(vector& cmds, std::string_view synopsis); static int clearZone(vector& cmds, std::string_view synopsis); +static int copyZone(vector& cmds, std::string_view synopsis); static int createBindDb(vector& cmds, std::string_view synopsis); static int createSecondaryZone(vector& cmds, std::string_view synopsis); static int createZone(vector& cmds, std::string_view synopsis); @@ -337,6 +338,9 @@ static const groupCommandDispatcher zoneMainCommands{ {"clear", {true, clearZone, "ZONE", "\tClear all records of a zone, but keep everything else"}}, + {"copy", {true, copyZone, + "ZONE NEW-ZONE", + "\tCreate zone NEW-ZONE with the contents of ZONE"}}, {"create", {true, createZone, "ZONE [NSNAME]", "\tCreate empty zone ZONE"}}, @@ -1818,6 +1822,107 @@ static int clearZone(const ZoneName &zone) { return EXIT_SUCCESS; } +// Copy the contents of zone `srcinfo` to zone `dstzone` in backend `tgt`. +// Used by both "zone copy" and "b2b-migrate". +static void copyZoneContents(const DomainInfo& srcinfo, const ZoneName& dstzone, DNSBackend* tgt) +{ + DNSBackend* src = srcinfo.backend; + size_t num_records{0}; + size_t num_comments{0}; + size_t num_metadata{0}; + size_t num_keys{0}; + bool rewriteNames{false}; + + DomainInfo dstinfo; + DNSResourceRecord rr; // NOLINT(readability-identifier-length) + + // Check target backend fits the requirements (only matters for b2b-migrate) + // TODO: figure a way to quickly know if there are comments and reject a + // target backend without comments support + if (srcinfo.zone.hasVariant() && (tgt->getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "Target backend does not support views." << endl; + throw PDNSException("Failed to create zone"); + } + + // Create zone + if (!tgt->createDomain(dstzone, srcinfo.kind, srcinfo.primaries, srcinfo.account)) { + throw PDNSException("Failed to create zone " + dstzone.toLogString()); + } + if (!tgt->getDomainInfo(dstzone, dstinfo)) { + throw PDNSException("Failed to create zone " + dstzone.toLogString()); + } + + // Copy records + if (!src->list(srcinfo.zone, srcinfo.id, true)) { + throw PDNSException("Failed to list records of " + srcinfo.zone.toLogString()); + } + + rewriteNames = srcinfo.zone != dstzone; + + tgt->startTransaction(dstzone, dstinfo.id); + + while(src->get(rr)) { + rr.domain_id = dstinfo.id; + if (rewriteNames) { + rr.qname.makeUsRelative(srcinfo.zone); + rr.qname += dstzone.operator const DNSName&(); + } + // FIXME: this should pass rr.ordername but only SQL-based backends + // will fill this field correctly. + if (!tgt->feedRecord(rr, DNSName())) { + tgt->abortTransaction(); + throw PDNSException("Failed to feed record '" + rr.qname.toLogString() + "' to zone " + dstzone.toLogString()); + } + num_records++; + } + + // Copy comments + if (src->listComments(srcinfo.id)) { + if ((tgt->getCapabilities() & DNSBackend::CAP_COMMENTS) == 0) { + tgt->abortTransaction(); + throw PDNSException("Target backend does not support comments - remove them first"); + } + Comment comm; + while(src->getComment(comm)) { + comm.domain_id = dstinfo.id; + if (rewriteNames) { + comm.qname.makeUsRelative(srcinfo.zone); + comm.qname += dstzone.operator const DNSName&(); + } + if (!tgt->feedComment(comm)) { + tgt->abortTransaction(); + throw PDNSException("Failed to feed zone comments"); + } + num_comments++; + } + } + + // Copy metadata + std::map> metas; + if (src->getAllDomainMetadata(srcinfo.zone, metas)) { + for (const auto& meta : metas) { + if (!tgt->setDomainMetadata(dstzone, meta.first, meta.second)) { + tgt->abortTransaction(); + throw PDNSException("Failed to feed zone metadata"); + } + num_metadata++; + } + } + + // Copy keys + int64_t keyID{-1}; // temp var for KeyID + std::vector keys; + if (src->getDomainKeys(srcinfo.zone, keys)) { + for(const DNSBackend::KeyData& key: keys) { + tgt->addDomainKey(dstzone, key, keyID); + num_keys++; + } + } + + tgt->commitTransaction(); + cout << "Copied " << num_records << " record(s), " << num_comments << " comment(s), " << num_metadata << " metadata(s) and " << num_keys << " cryptokey(s)" << endl; +} + class PDNSColors { public: @@ -2487,6 +2592,44 @@ static int createZone(const ZoneName &zone, const DNSName& nsname) { return EXIT_SUCCESS; } +static int copyZone(vector& cmds, const std::string_view synopsis) +{ + if(cmds.size() != 2) { + return usage(synopsis); + } + + ZoneName src(cmds.at(0)); + ZoneName dst(cmds.at(1)); + + UtilBackend B; //NOLINT(readability-identifier-length) + DomainInfo srcinfo; + DomainInfo dstinfo; + if (B.getDomainInfo(dst, dstinfo)) { + cerr << "Zone '" << dst << "' already exists." << endl; + return EXIT_FAILURE; + } + if ((B.getCapabilities() & DNSBackend::CAP_CREATE) == 0) { + cerr << "None of the configured backends support zone creation." << endl; + cerr << "Zone '" << dst << "' was not created." << endl; + return EXIT_FAILURE; + } + if (dst.hasVariant() && (B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "None of the configured backends support views." << endl; + cerr << "Zone '" << dst << "' was not created." << endl; + return EXIT_FAILURE; + } + if (!B.getDomainInfo(src, srcinfo)) { + cerr << "Zone '" << src << "' does not exist" << endl; + return EXIT_FAILURE; + } + cout << "Creating '" << dst << "'" << endl; + copyZoneContents(srcinfo, dst, srcinfo.backend); + + cout << "Remember to check the contents of '" << dst << "' and rectify the new zone." << endl; + + return EXIT_SUCCESS; +} + // add-record ZONE name type [ttl] "content" ["content"] static int addOrReplaceRecord(bool isAdd, const vector& cmds) { @@ -5230,79 +5373,9 @@ static int B2BMigrate(vector& cmds, const std::string_view synopsis) src->getAllDomains(&domains, false, true); // iterate zones for(const DomainInfo& di: domains) { // NOLINT(readability-identifier-length) - size_t nr{0}; // NOLINT(readability-identifier-length) - size_t nc{0}; // NOLINT(readability-identifier-length) - size_t nm{0}; // NOLINT(readability-identifier-length) - size_t nk{0}; // NOLINT(readability-identifier-length) - DomainInfo di_new; - DNSResourceRecord rr; // NOLINT(readability-identifier-length) cout<<"Processing '"<getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { - cerr << "Target backend does not support views." << endl; - throw PDNSException("Failed to create zone"); - } - if (!tgt->createDomain(di.zone, di.kind, di.primaries, di.account)) { - throw PDNSException("Failed to create zone"); - } - if (!tgt->getDomainInfo(di.zone, di_new)) { - throw PDNSException("Failed to create zone"); - } - // move records - if (!src->list(di.zone, di.id, true)) { - throw PDNSException("Failed to list records"); - } - nr=0; - - tgt->startTransaction(di.zone, di_new.id); - while(src->get(rr)) { - rr.domain_id = di_new.id; - if (!tgt->feedRecord(rr, DNSName())) { - throw PDNSException("Failed to feed record"); - } - nr++; - } - - // move comments - nc=0; - if (src->listComments(di.id)) { - if ((tgt->getCapabilities() & DNSBackend::CAP_COMMENTS) == 0) { - throw PDNSException("Target backend does not support comments - remove them first"); - } - Comment c; // NOLINT(readability-identifier-length) - while(src->getComment(c)) { - c.domain_id = di_new.id; - if (!tgt->feedComment(c)) { - throw PDNSException("Failed to feed zone comments"); - } - nc++; - } - } - // move metadata - nm=0; - std::map > meta; - if (src->getAllDomainMetadata(di.zone, meta)) { - for (const auto& i : meta) { // NOLINT(readability-identifier-length) - if (!tgt->setDomainMetadata(di.zone, i.first, i.second)) { - throw PDNSException("Failed to feed zone metadata"); - } - nm++; - } - } - // move keys - nk=0; - // temp var for KeyID - int64_t keyID{-1}; - std::vector keys; - if (src->getDomainKeys(di.zone, keys)) { - for(const DNSBackend::KeyData& k: keys) { // NOLINT(readability-identifier-length) - tgt->addDomainKey(di.zone, k, keyID); - nk++; - } - } - tgt->commitTransaction(); - cout<<"Moved "<& cmds, const std::string_view synopsis) } cout<<"Moved "<