pdnsutil set-catalog example.com catalog.example
pdnsutil set-kind example.com primary
+Since version 5.1.0 a catalog zone can be a member of another catalog.
+
Setting catalog values is supported in the :doc:`API <http-api/zone>`, by setting the ``catalog`` property in the zone properties.
Setting the catalog to an empty ``""`` removes the member zone from the catalog it is in.
unlimited. Note that exchanges related to an AXFR or IXFR are not
affected by this setting.
+.. _setting-member-catalog-group
+
+``member-catalog-group``
+------------------------
+
+- String
+- Default: pdns-member-catalog
+
+.. versionadded:: 5.1.0
+
+Catalog group used to signal that a member zone is a catalog.
+
.. _setting-module-dir:
``module-dir``
declare(suffix, "update-serial-query", "", "update domains set notified_serial=? where id=?");
declare(suffix, "update-lastcheck-query", "", "update domains set last_check=? where id=?");
declare(suffix, "info-all-primary-query", "", "select d.id, d.name, d.type, d.notified_serial,d.options, d.catalog,r.content from records r join domains d on r.domain_id=d.id and r.name=d.name where r.type='SOA' and r.disabled=0 and d.type in ('MASTER', 'PRODUCER') order by d.id");
- declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=? and records.type='SOA' and records.disabled=0");
- declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=?");
+ declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=? and records.type='SOA' and records.disabled=0");
+ declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=?");
declare(suffix, "delete-domain-query", "", "delete from domains where name=?");
declare(suffix, "delete-zone-query", "", "delete from records where domain_id=?");
declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=? and name=? and type=?");
declare(suffix, "update-serial-query", "", "update domains set notified_serial=? where id=?");
declare(suffix, "update-lastcheck-query", "", "update domains set last_check=? where id=?");
declare(suffix, "info-all-primary-query", "", "select domains.id, domains.name, domains.type, domains.notified_serial, domains.options, domains.catalog, records.content from records join domains on records.domain_id=domains.id and records.name=domains.name where records.type='SOA' and records.disabled=0 and domains.type in ('MASTER', 'PRODUCER') order by domains.id");
- declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=? and records.type='SOA' and records.disabled=0");
- declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=?");
+ declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=? and records.type='SOA' and records.disabled=0");
+ declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=?");
declare(suffix, "delete-domain-query", "", "delete from domains where name=?");
declare(suffix, "delete-zone-query", "", "delete from records where domain_id=?");
declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=? and name=? and type=?");
declare(suffix, "update-serial-query", "", "update domains set notified_serial=$1 where id=$2");
declare(suffix, "update-lastcheck-query", "", "update domains set last_check=$1 where id=$2");
declare(suffix, "info-all-primary-query", "", "select domains.id, domains.name, domains.type, domains.notified_serial, domains.options, domains.catalog, records.content from records join domains on records.domain_id=domains.id and records.name=domains.name where records.type='SOA' and records.disabled=false and domains.type in ('MASTER', 'PRODUCER') order by domains.id");
- declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=$1 and records.type='SOA' and records.disabled=false");
- declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=$1");
+ declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=$1 and records.type='SOA' and records.disabled=false");
+ declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=$1");
declare(suffix, "delete-domain-query", "", "delete from domains where name=$1");
declare(suffix, "delete-zone-query", "", "delete from records where domain_id=$1");
declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=$1 and name=$2 and type=$3");
declare(suffix, "update-serial-query", "", "update domains set notified_serial=:serial where id=:domain_id");
declare(suffix, "update-lastcheck-query", "", "update domains set last_check=:last_check where id=:domain_id");
declare(suffix, "info-all-primary-query", "", "select domains.id, domains.name, domains.type, domains.notified_serial, domains.options, domains.catalog, records.content from records join domains on records.domain_id=domains.id and records.name=domains.name where records.type='SOA' and records.disabled=0 and domains.type in ('MASTER', 'PRODUCER') order by domains.id");
- declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=:catalog and records.type='SOA' and records.disabled=0");
- declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=:catalog");
+ declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=:catalog and records.type='SOA' and records.disabled=0");
+ declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=:catalog");
declare(suffix, "delete-domain-query", "", "delete from domains where name=:domain");
declare(suffix, "delete-zone-query", "", "delete from records where domain_id=:domain_id");
declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=:domain_id and name=:qname and type=:qtype");
try {
getAllDomainsFiltered(&scratch, [this, &catalog, &members, &type](DomainInfo& di) {
- if ((type == CatalogInfo::CatalogType::Producer && di.kind != DomainInfo::Primary) || (type == CatalogInfo::CatalogType::Consumer && di.kind != DomainInfo::Secondary) || di.catalog != catalog) {
+ if ((type == CatalogInfo::CatalogType::Producer && !di.isPrimaryType()) || (type == CatalogInfo::CatalogType::Consumer && !di.isSecondaryType()) || di.catalog != catalog) {
return false;
}
+ if (di.isCatalogType() && di.zone == di.catalog) {
+ SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog '" << di.zone << "' cannot be a member of itself" << endl,
+ d_slog->info(Logr::Warning, "catalog cannot be a member of itself", "catalog", Logging::Loggable(di.zone)));
+ members.clear();
+ throw getCatalogMembersReturnFalseException();
+ }
+
CatalogInfo ci;
ci.d_id = di.id;
ci.d_zone = di.zone;
ci.d_primaries = di.primaries;
try {
ci.fromJson(di.options, type);
+ if (di.isCatalogType()) {
+ ci.addGroup(g_memberCatalogGroup);
+ }
}
catch (const std::runtime_error& e) {
SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " options '" << di.options << "' for zone '" << di.zone << "' is no valid JSON: " << e.what() << endl,
void CatalogInfo::updateCatalogHash(CatalogHashMap& hashes, const DomainInfo& di)
{
CatalogInfo ci;
- hashes[di.catalog].process(std::to_string(di.id) + di.zone.toLogString());
+ hashes[di.catalog].process(std::to_string(di.id) + di.zone.toLogString() + DomainInfo::getKindString(di.kind));
if (ci.parseJson(di.options, CatalogType::Producer)) {
hashes[di.catalog].process(ci.d_doc["producer"].dump());
}
void fromJson(const std::string& json, CatalogType type);
std::string toJson() const;
void setType(CatalogType type) { d_type = type; }
+ void addGroup(const std::string& group) { d_group.insert(group); }
static void updateCatalogHash(CatalogHashMap& hashes, const DomainInfo& di);
DNSName getUnique() const { return DNSName(toBase32Hex(hashQNameWithSalt(std::to_string(d_id), 0, DNSName(d_zone)))); } // salt with domain id to detect recreated zones
bool parseJson(const std::string& json, CatalogType type);
};
+
+extern std::string g_memberCatalogGroup;
bool g_views;
bool g_slogStructured{false};
static Logger::Urgency s_logUrgency;
+std::string g_memberCatalogGroup;
typedef Distributor<DNSPacket, DNSPacket, PacketHandler> DNSDistributor;
ArgvMap theArg;
::arg().setSwitch("consistent-backends", "Assume individual zones are not divided over backends. Send only ANY lookup operations to the backend to reduce the number of lookups") = "yes";
::arg().set("default-catalog-zone", "Catalog zone to assign newly created primary zones (via the API) to") = "";
+ ::arg().set("member-catalog-group", "Catalog group used to signal that a member zone is a catalog") = "pdns-member-catalog";
#ifdef ENABLE_GSS_TSIG
::arg().setSwitch("enable-gss-tsig", "Enable GSS TSIG processing") = "no";
g_doGssTSIG = ::arg().mustDo("enable-gss-tsig");
#endif
g_views = ::arg().mustDo("views");
+ g_memberCatalogGroup = ::arg()["member-catalog-group"];
DNSPacket::s_udpTruncationThreshold = std::max(512, ::arg().asNum("udp-truncation-threshold"));
DNSPacket::s_doEDNSSubnetProcessing = ::arg().mustDo("edns-subnet-processing");
CatalogInfo ciDB = *db;
if (ciDB.d_unique.empty() || ciXFR.d_unique == ciDB.d_unique) { // update
bool doOptions{false};
+ bool doType{false};
if (ciDB.d_unique.empty()) { // set unique
SLOG(g_log << Logger::Warning << ctx.logPrefix << "set unique, zone '" << ciXFR.d_zone << "' is now a member" << endl,
if (ciXFR.d_group != ciDB.d_group) { // update group
SLOG(g_log << Logger::Warning << ctx.logPrefix << "update group for zone '" << ciXFR.d_zone << "' to '" << boost::join(ciXFR.d_group, ", ") << "'" << endl,
ctx.slog->info(Logr::Warning, "Catalog-Zone: update group", "zone", Logging::Loggable(ciXFR.d_zone), "group", Logging::Loggable(boost::join(ciXFR.d_group, ", ")))); // can't apply Logging::IterLoggable on set
+ doType = !empty(g_memberCatalogGroup) && ((ciXFR.d_group.count(g_memberCatalogGroup) != 0) != (ciDB.d_group.count(g_memberCatalogGroup) != 0));
ciDB.d_group = ciXFR.d_group;
doOptions = true;
}
ctx.domain.backend->setOptions(ciXFR.d_zone, ciDB.toJson());
}
+ if (doType) { // update zone type
+ if (doTransaction && (inTransaction = ctx.domain.backend->startTransaction(ctx.domain.zone))) {
+ SLOG(g_log << Logger::Warning << ctx.logPrefix << "backend transaction started" << endl,
+ ctx.slog->info(Logr::Warning, "Catalog-Zone: backend transaction started"));
+ doTransaction = false;
+ }
+
+ DomainInfo::DomainKind kind = ciXFR.d_group.count(g_memberCatalogGroup) != 0 ? DomainInfo::Consumer : DomainInfo::Secondary;
+ SLOG(g_log << Logger::Warning << ctx.logPrefix << "update type to '" << DomainInfo::getKindString(kind) << "' for zone '" << ciXFR.d_zone << "'" << endl,
+ ctx.slog->info(Logr::Warning, "Catalog-Zone: update type", "zone", Logging::Loggable(ciXFR.d_zone), "type", Logging::Loggable(kind)));
+ ctx.domain.backend->setKind(ciXFR.d_zone, kind);
+ }
+
if (ctx.domain.primaries != ciDB.d_primaries) { // update primaries
if (doTransaction && (inTransaction = ctx.domain.backend->startTransaction(ctx.domain.zone))) {
SLOG(g_log << Logger::Warning << ctx.logPrefix << "backend transaction started" << endl,
SLOG(g_log << Logger::Warning << ctx.logPrefix << "create zone '" << ciCreate.d_zone << "'" << endl,
ctx.slog->info(Logr::Warning, "Catalog-Zone: create zone", "zone", Logging::Loggable(ciCreate.d_zone)));
- ctx.domain.backend->createDomain(ciCreate.d_zone, DomainInfo::Secondary, ciCreate.d_primaries, "");
+ d.kind = !empty(g_memberCatalogGroup) && ciCreate.d_group.count(g_memberCatalogGroup) != 0 ? DomainInfo::Consumer : DomainInfo::Secondary;
+ ctx.domain.backend->createDomain(ciCreate.d_zone, d.kind, ciCreate.d_primaries, "");
ctx.domain.backend->setPrimaries(ciCreate.d_zone, ctx.domain.primaries);
ctx.domain.backend->setOptions(ciCreate.d_zone, ciCreate.toJson());
return false;
}
- // Get catalog ifo from db
+ // Get catalog info from db
if (!ctx.domain.backend->getCatalogMembers(ctx.domain.zone, fromDB, CatalogInfo::CatalogType::Consumer)) {
return false;
}
void GSQLBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::unordered_set<DNSName>& catalogs, CatalogHashMap& catalogHashes)
{
/*
- list all domains that need notifications for which we are promary, and insert into
+ list all domains that need notifications for which we are primary, and insert into
updatedDomains: id, name, notified_serial, serial
*/
continue;
}
+ di.kind = DomainInfo::stringToKind(row[2]);
+
try {
pdns::checked_stoi_into(di.id, row[0]);
}
di.catalog = ZoneName(row[5]);
}
catch (const std::runtime_error& e) {
- SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " zone name '" << row[5] << "' is not a valid DNS name: " << e.what() << endl,
- d_slog->error(Logr::Warning, e.what(), "zone name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
+ SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog name '" << row[5] << "' is not a valid DNS name: " << e.what() << endl,
+ d_slog->error(Logr::Warning, e.what(), "catalog name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
continue;
}
catch (PDNSException& ae) {
- SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " zone name '" << row[5] << "' is not a valid DNS name: " << ae.reason << endl,
- d_slog->error(Logr::Warning, ae.reason, "zone name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
+ SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog name '" << row[5] << "' is not a valid DNS name: " << ae.reason << endl,
+ d_slog->error(Logr::Warning, ae.reason, "catalog name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
continue;
}
- if (pdns_iequals(row[2], "PRODUCER")) {
+ if (di.kind == DomainInfo::Producer) {
catalogs.insert(di.zone.operator const DNSName&());
catalogHashes[di.zone].process("");
- continue; // Producer freshness check is performed elsewhere
+ if (di.catalog.empty()) {
+ continue; // Producer is no catalog member, freshness check is performed elsewhere
+ }
}
- else if (!pdns_iequals(row[2], "MASTER")) {
+ else if (di.kind != DomainInfo::Primary) {
SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " type '" << row[2] << "' for zone '" << di.zone << "' is no primary type" << endl,
d_slog->info(Logr::Warning, "zone type is not fit for a primary", "zone", Logging::Loggable(di.zone), "type", Logging::Loggable(row[2])));
+ continue;
}
try {
continue;
}
+ if (di.kind == DomainInfo::Producer) {
+ continue; // Producer is a catalog member, freshness check is performed elsewhere
+ }
+
try {
pdns::checked_stoi_into(di.notified_serial, row[3]);
}
}
members.reserve(d_result.size());
- for (const auto& row : d_result) { // id, zone, options, [master]
+ for (const auto& row : d_result) { // id, zone, type, options, [master]
if (type == CatalogInfo::CatalogType::Producer) {
- ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 3);
+ ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 4);
}
else {
- ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 4);
+ ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 5);
}
CatalogInfo ci;
return false;
}
+ auto kind = DomainInfo::stringToKind(row[2]);
+ auto isCatalog = kind == DomainInfo::Producer || kind == DomainInfo::Consumer;
+
+ if (isCatalog && ci.d_zone == catalog) {
+ SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog '" << ci.d_zone << "' cannot be a member of itself" << endl,
+ d_slog->info(Logr::Warning, "catalog cannot be a member of itself", "catalog", Logging::Loggable(ci.d_zone)));
+ members.clear();
+ return false;
+ }
+
try {
pdns::checked_stoi_into(ci.d_id, row[0]);
}
}
try {
- ci.fromJson(row[2], type);
+ ci.fromJson(row[3], type);
+ if (isCatalog) {
+ ci.addGroup(g_memberCatalogGroup);
+ }
}
catch (const std::runtime_error& e) {
- SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " options '" << row[2] << "' for zone '" << ci.d_zone << "' is no valid JSON: " << e.what() << endl,
- d_slog->error(Logr::Warning, e.what(), "catalog 'options' field is not valid JSON", "zone", Logging::Loggable(ci.d_zone), "field", Logging::Loggable(row[2])));
+ SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " options '" << row[3] << "' for zone '" << ci.d_zone << "' is no valid JSON: " << e.what() << endl,
+ d_slog->error(Logr::Warning, e.what(), "catalog 'options' field is not valid JSON", "zone", Logging::Loggable(ci.d_zone), "field", Logging::Loggable(row[3])));
members.clear();
return false;
}
- if (row.size() >= 4) { // Consumer only
+ if (type == CatalogInfo::CatalogType::Consumer) { // Consumer only
vector<string> primaries;
- stringtok(primaries, row[3], ", \t");
+ stringtok(primaries, row[4], ", \t");
for (const auto& m : primaries) {
try {
ci.d_primaries.emplace_back(m, 53);
AuthQueryCache QC;
AuthZoneCache g_zoneCache;
uint16_t g_maxNSEC3Iterations{0};
+std::string g_memberCatalogGroup;
namespace po = boost::program_options;
po::variables_map g_vm;
$PDNSUTIL --config-dir=. --config-name=gmysql load-zone catalog.invalid zones/catalog.invalid
$PDNSUTIL --config-dir=. --config-name=gmysql set-kind catalog.invalid producer
+ $PDNSUTIL --config-dir=. --config-name=gmysql load-zone catalog2.invalid zones/catalog2.invalid
+ $PDNSUTIL --config-dir=. --config-name=gmysql set-kind catalog2.invalid producer
+ $PDNSUTIL --config-dir=. --config-name=gmysql set-catalog catalog2.invalid catalog.invalid
+ $PDNSUTIL --config-dir=. --config-name=gmysql set-catalog minimal.com catalog2.invalid
+
$PDNSUTIL --config-dir=. --config-name=gmysql set-option test.com producer coo other-catalog.invalid
$PDNSUTIL --config-dir=. --config-name=gmysql set-option test.com producer unique 123
$PDNSUTIL --config-dir=. --config-name=gmysql set-option tsig.com producer group pdns-group-x pdns-group-y
# setup catalog zone
if [ $zones -ne 1 ] # detect root tests
then
- zones=$((zones+1))
+ zones=$((zones+2))
$PDNSUTIL --config-dir=. --config-name=gmysql2 create-secondary-zone catalog.invalid 127.0.0.1:$port
$PDNSUTIL --config-dir=. --config-name=gmysql2 set-kind catalog.invalid consumer
$RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb load-zone catalog.invalid zones/catalog.invalid
$RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-kind catalog.invalid producer
+ $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb load-zone catalog2.invalid zones/catalog2.invalid
+ $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-kind catalog2.invalid producer
+ $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-catalog catalog2.invalid catalog.invalid
+ $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-catalog minimal.com$plusvariant catalog2.invalid
+
$RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-options-json test.com$plusvariant '{"producer":{"coo":"other-catalog.invalid","unique":"123"}}'
$RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-options-json tsig.com$plusvariant '{"producer":{"group":["pdns-group-x","pdns-group-y"]}}'
fi
# setup catalog zone
if [ $zones -ne 1 ] # detect root tests
then
- zones=$((zones+1))
+ zones=$((zones+2))
$PDNSUTIL --config-dir=. --config-name=lmdb2 create-secondary-zone catalog.invalid 127.0.0.1:$port
$PDNSUTIL --config-dir=. --config-name=lmdb2 set-kind catalog.invalid consumer
..myroot
2.0.192.in-addr.arpa
catalog.invalid
+catalog2.invalid
cdnskey-cds-test.com
cryptokeys.org
delegated.dnssec-parent.com
--- /dev/null
+$TTL 3600
+$ORIGIN catalog2.invalid.
+@ IN SOA ns1.zone.invalid. hostmaster.zone.invalid. ( 1
+ 1M ; refresh
+ 30S ; retry
+ 1W ; expire
+ 1D ; default_ttl
+ )
+
+@ IN NS ns1.zone.invalid.