From: Christian Hofstaedtler Date: Tue, 25 Feb 2014 12:29:32 +0000 (+0100) Subject: Add per-rrset-comments X-Git-Tag: rec-3.6.0-rc1~160^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6cc98ddf441cff45f72abc299c97874e0eba6a49;p=thirdparty%2Fpdns.git Add per-rrset-comments --- diff --git a/modules/gmysqlbackend/gmysqlbackend.cc b/modules/gmysqlbackend/gmysqlbackend.cc index 97e1aa667b..159fde7bd6 100644 --- a/modules/gmysqlbackend/gmysqlbackend.cc +++ b/modules/gmysqlbackend/gmysqlbackend.cc @@ -142,6 +142,11 @@ public: declare(suffix,"get-tsig-keys-query","", "select name,algorithm, secret from tsigkeys"); declare(suffix, "get-all-domains-query", "Retrieve all domains", "select records.domain_id, records.name, records.content, domains.type, domains.master, domains.notified_serial, domains.last_check from records, domains where records.domain_id=domains.id and records.type='SOA' and (records.disabled=0 OR %d)"); + + declare(suffix, "list-comments-query", "", "SELECT domain_id,name,type,modified_at,account,comment FROM comments WHERE domain_id=%d"); + declare(suffix, "insert-comment-query", "", "INSERT INTO comments (domain_id, name, type, modified_at, account, comment) VALUES (%d, '%s', '%s', %d, '%s', '%s')"); + declare(suffix, "delete-comment-rrset-query", "", "DELETE FROM comments WHERE domain_id=%d AND name='%s' AND type='%s'"); + declare(suffix, "delete-comments-query", "", "DELETE FROM comments WHERE domain_id=%d"); } DNSBackend *make(const string &suffix="") diff --git a/modules/gmysqlbackend/no-dnssec.schema.mysql.sql b/modules/gmysqlbackend/no-dnssec.schema.mysql.sql index 9636ad5303..02568aadfd 100644 --- a/modules/gmysqlbackend/no-dnssec.schema.mysql.sql +++ b/modules/gmysqlbackend/no-dnssec.schema.mysql.sql @@ -33,3 +33,18 @@ create table supermasters ( account VARCHAR(40) DEFAULT NULL, PRIMARY KEY (ip, nameserver) ) Engine=InnoDB; + +CREATE TABLE comments ( + id INT auto_increment, + domain_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) NOT NULL, + comment VARCHAR(64000) NOT NULL, + primary key(id) +) Engine=InnoDB; + +CREATE INDEX comments_domain_id_idx ON comments (domain_id); +CREATE INDEX comments_name_type_idx ON comments (name, type); +CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); diff --git a/modules/goraclebackend/goracle-schema.sql b/modules/goraclebackend/goracle-schema.sql index bb902e8669..812a515248 100644 --- a/modules/goraclebackend/goracle-schema.sql +++ b/modules/goraclebackend/goracle-schema.sql @@ -42,6 +42,21 @@ create table supermasters ( ); +CREATE TABLE comments ( + id number(11) not NULL, + domain_id INT NOT NULL REFERENCES Domains(ID) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) NOT NULL, + comment VARCHAR2(4000) NOT NULL +); +CREATE INDEX comments$nametype ON comments (name, type); +CREATE INDEX comments$domain_id ON comments (domain_id); +CREATE INDEX comments$order ON comments (domain_id, modified_at); +CREATE SEQUENCE comments_id_sequence; + + create table domainmetadata ( id NUMBER, domain_id INT NOT NULL, diff --git a/modules/goraclebackend/goraclebackend.cc b/modules/goraclebackend/goraclebackend.cc index 4b629ef5f6..5a21a1f532 100644 --- a/modules/goraclebackend/goraclebackend.cc +++ b/modules/goraclebackend/goraclebackend.cc @@ -149,6 +149,11 @@ public: declare(suffix,"get-tsig-keys-query","", "select name,algorithm, secret from tsigkeys"); declare(suffix, "get-all-domains-query", "Retrieve all domains", "select records.domain_id, records.name, records.content, domains.type, domains.master, domains.notified_serial, domains.last_check from records, domains where records.domain_id=domains.id and records.type='SOA' and (records.disabled=0 OR 1=%d)"); + + declare(suffix, "list-comments-query", "", "SELECT domain_id,name,type,modified_at,account,comment FROM comments WHERE domain_id=%d"); + declare(suffix, "insert-comment-query", "", "INSERT INTO comments (id, domain_id, name, type, modified_at, account, comment) VALUES (comments_id_sequence.nextval, %d, '%s', '%s', %d, '%s', '%s')"); + declare(suffix, "delete-comment-rrset-query", "", "DELETE FROM comments WHERE domain_id=%d AND name='%s' AND type='%s'"); + declare(suffix, "delete-comments-query", "", "DELETE FROM comments WHERE domain_id=%d"); } DNSBackend* make(const string &suffix="") { diff --git a/modules/gpgsqlbackend/gpgsqlbackend.cc b/modules/gpgsqlbackend/gpgsqlbackend.cc index 92e7c111a0..c9667cbffa 100644 --- a/modules/gpgsqlbackend/gpgsqlbackend.cc +++ b/modules/gpgsqlbackend/gpgsqlbackend.cc @@ -137,6 +137,11 @@ public: declare(suffix,"get-tsig-keys-query","", "select name,algorithm, secret from tsigkeys"); declare(suffix, "get-all-domains-query", "Retrieve all domains", "select records.domain_id, records.name, records.content, domains.type, domains.master, domains.notified_serial, domains.last_check from records, domains where records.domain_id=domains.id and records.type='SOA' and (records.disabled=false OR %d::bool)"); + + declare(suffix, "list-comments-query", "", "SELECT domain_id,name,type,modified_at,account,comment FROM comments WHERE domain_id=%d"); + declare(suffix, "insert-comment-query", "", "INSERT INTO comments (domain_id, name, type, modified_at, account, comment) VALUES (%d, E'%s', E'%s', %d, E'%s', E'%s')"); + declare(suffix, "delete-comment-rrset-query", "", "DELETE FROM comments WHERE domain_id=%d AND name=E'%s' AND type=E'%s'"); + declare(suffix, "delete-comments-query", "", "DELETE FROM comments WHERE domain_id=%d"); } DNSBackend *make(const string &suffix="") diff --git a/modules/gpgsqlbackend/no-dnssec.schema.pgsql.sql b/modules/gpgsqlbackend/no-dnssec.schema.pgsql.sql index 9f9560cbed..e41e363492 100644 --- a/modules/gpgsqlbackend/no-dnssec.schema.pgsql.sql +++ b/modules/gpgsqlbackend/no-dnssec.schema.pgsql.sql @@ -36,3 +36,20 @@ create table supermasters ( account VARCHAR(40) DEFAULT NULL, PRIMARY KEY (ip, nameserver) ); + +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + domain_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) DEFAULT NULL, + comment VARCHAR(65535) NOT NULL, + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK (((name)::text = lower((name)::text))) +); +CREATE INDEX comments_domain_id_idx ON comments (domain_id); +CREATE INDEX comments_name_type_idx ON comments (name, type); +CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); diff --git a/modules/gsqlite3backend/gsqlite3backend.cc b/modules/gsqlite3backend/gsqlite3backend.cc index 0e0bb34822..b602efae3b 100644 --- a/modules/gsqlite3backend/gsqlite3backend.cc +++ b/modules/gsqlite3backend/gsqlite3backend.cc @@ -150,6 +150,11 @@ public: declare(suffix,"get-tsig-keys-query","", "select name,algorithm, secret from tsigkeys"); declare(suffix, "get-all-domains-query", "Retrieve all domains", "select records.domain_id, records.name, records.content, domains.type, domains.master, domains.notified_serial, domains.last_check from records, domains where records.domain_id=domains.id and records.type='SOA' and (records.disabled=0 OR %d)"); + + declare(suffix, "list-comments-query", "", "SELECT domain_id,name,type,modified_at,account,comment FROM comments WHERE domain_id=%d"); + declare(suffix, "insert-comment-query", "", "INSERT INTO comments (domain_id, name, type, modified_at, account, comment) VALUES (%d, '%s', '%s', %d, '%s', '%s')"); + declare(suffix, "delete-comment-rrset-query", "", "DELETE FROM comments WHERE domain_id=%d AND name='%s' AND type='%s'"); + declare(suffix, "delete-comments-query", "", "DELETE FROM comments WHERE domain_id=%d"); } //! Constructs a new gSQLite3Backend object. diff --git a/modules/gsqlite3backend/no-dnssec.schema.sqlite3.sql b/modules/gsqlite3backend/no-dnssec.schema.sqlite3.sql index 0abcccb32b..fced5c7aa0 100644 --- a/modules/gsqlite3backend/no-dnssec.schema.sqlite3.sql +++ b/modules/gsqlite3backend/no-dnssec.schema.sqlite3.sql @@ -33,3 +33,16 @@ create table supermasters ( ); CREATE UNIQUE INDEX ip_nameserver_pk ON supermasters(ip, nameserver); + +CREATE TABLE comments ( + id INTEGER PRIMARY KEY, + domain_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) DEFAULT NULL, + comment VARCHAR(65535) NOT NULL +); +CREATE INDEX comments_domain_id_index ON comments (domain_id); +CREATE INDEX comments_nametype_index ON comments (name, type); +CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); diff --git a/pdns/Makefile.am b/pdns/Makefile.am index 67b0f2e2b2..92a79c9c98 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -62,7 +62,8 @@ randomhelper.cc namespaces.hh nsecrecords.cc base32.cc dbdnsseckeeper.cc dnsseci dnsseckeeper.hh dnssecinfra.hh base32.hh dns.cc dnssecsigner.cc polarrsakeyinfra.cc \ sha.hh md5.hh signingpipe.cc signingpipe.hh dnslabeltext.cc lua-pdns.cc lua-auth.cc lua-auth.hh serialtweaker.cc \ ednssubnet.cc ednssubnet.hh cachecleaner.hh json.cc json.hh \ -version.hh version.cc rfc2136handler.cc responsestats.cc responsestats.hh +version.hh version.cc rfc2136handler.cc responsestats.cc responsestats.hh \ +comment.hh pdns_server_LDFLAGS=@moduleobjects@ @modulelibs@ @DYNLINKFLAGS@ @LIBDL@ @THREADFLAGS@ $(BOOST_SERIALIZATION_LDFLAGS) -rdynamic diff --git a/pdns/backends/gsql/gsqlbackend.cc b/pdns/backends/gsql/gsqlbackend.cc index 164c819dc6..ed3b32a140 100644 --- a/pdns/backends/gsql/gsqlbackend.cc +++ b/pdns/backends/gsql/gsqlbackend.cc @@ -311,6 +311,11 @@ GSQLBackend::GSQLBackend(const string &mode, const string &suffix) d_removeEmptyNonTerminalsFromZoneQuery = getArg("remove-empty-non-terminals-from-zone-query"); d_insertEmptyNonTerminalQuery = getArg("insert-empty-non-terminal-query"+authswitch); d_deleteEmptyNonTerminalQuery = getArg("delete-empty-non-terminal-query"); + + d_ListCommentsQuery = getArg("list-comments-query"); + d_InsertCommentQuery = getArg("insert-comment-query"); + d_DeleteCommentRRsetQuery = getArg("delete-comment-rrset-query"); + d_DeleteCommentsQuery = getArg("delete-comments-query"); if (d_dnssecQueries) { @@ -985,6 +990,7 @@ bool GSQLBackend::deleteDomain(const string &domain) string recordsQuery = (boost::format(d_DeleteZoneQuery) % di.id).str(); string metadataQuery; string keysQuery; + string commentsQuery = (boost::format(d_DeleteCommentsQuery) % di.id).str(); string domainQuery = (boost::format(d_DeleteDomainQuery) % sqlDomain).str(); if (d_dnssecQueries) { @@ -998,6 +1004,7 @@ bool GSQLBackend::deleteDomain(const string &domain) d_db->doCommand(metadataQuery); d_db->doCommand(keysQuery); } + d_db->doCommand(commentsQuery); d_db->doCommand(domainQuery); } catch(SSqlException &e) { @@ -1098,6 +1105,15 @@ bool GSQLBackend::replaceRRSet(uint32_t domain_id, const string& qname, const QT ).str(); } d_db->doCommand(query); + if (rrset.empty()) { + // zap comments for now non-existing rrset + query = (boost::format(d_DeleteCommentRRsetQuery) + % domain_id + % sqlEscape(qname) + % sqlEscape(qt.getName()) + ).str(); + d_db->doCommand(query); + } BOOST_FOREACH(const DNSResourceRecord& rr, rrset) { feedRecord(rr); } @@ -1285,3 +1301,75 @@ bool GSQLBackend::calculateSOASerial(const string& domain, const SOAData& sd, ti return false; } + +bool GSQLBackend::listComments(const uint32_t domain_id) +{ + string query = (boost::format(d_ListCommentsQuery) + % domain_id + ).str(); + + try { + d_db->doQuery(query); + } + catch(SSqlException &e) { + throw PDNSException("GSQLBackend list comments query: "+e.txtReason()); + } + + return true; +} + +bool GSQLBackend::getComment(Comment& comment) +{ + SSql::row_t row; + + if (!d_db->getRow(row)) { + return false; + } + + // domain_id,name,type,modified_at,account,comment + comment.domain_id = atol(row[0].c_str()); + comment.qname = row[1]; + comment.qtype = row[2]; + comment.modified_at = atol(row[3].c_str()); + comment.account = row[4]; + comment.content = row[5]; + + return true; +} + +void GSQLBackend::feedComment(const Comment& comment) +{ + string query = (boost::format(d_InsertCommentQuery) + % comment.domain_id + % toLower(sqlEscape(comment.qname)) + % sqlEscape(comment.qtype.getName()) + % comment.modified_at + % sqlEscape(comment.account) + % sqlEscape(comment.content) + ).str(); + + try { + d_db->doCommand(query); + } + catch (SSqlException &e) { + throw PDNSException("GSQLBackend unable to feed comment: "+e.txtReason()); + } +} + +bool GSQLBackend::replaceComments(const uint32_t domain_id, const string& qname, const QType& qt, const vector& comments) +{ + string query; + query = (boost::format(d_DeleteCommentRRsetQuery) + % domain_id + % toLower(sqlEscape(qname)) + % sqlEscape(qt.getName()) + ).str(); + + d_db->doCommand(query); + + BOOST_FOREACH(const Comment& comment, comments) { + feedComment(comment); + } + + return true; +} diff --git a/pdns/backends/gsql/gsqlbackend.hh b/pdns/backends/gsql/gsqlbackend.hh index c28d0f4390..9b86cb5d74 100644 --- a/pdns/backends/gsql/gsqlbackend.hh +++ b/pdns/backends/gsql/gsqlbackend.hh @@ -80,6 +80,11 @@ public: bool deleteTSIGKey(const string& name); bool getTSIGKeys(std::vector< struct TSIGKey > &keys); + bool listComments(const uint32_t domain_id); + bool getComment(Comment& comment); + void feedComment(const Comment& comment); + bool replaceComments(const uint32_t domain_id, const string& qname, const QType& qt, const vector& comments); + private: string d_qname; SSql *d_db; @@ -154,6 +159,11 @@ private: string d_getAllDomainsQuery; + string d_ListCommentsQuery; + string d_InsertCommentQuery; + string d_DeleteCommentRRsetQuery; + string d_DeleteCommentsQuery; + protected: bool d_dnssecQueries; }; diff --git a/pdns/comment.hh b/pdns/comment.hh new file mode 100644 index 0000000000..1587d8b498 --- /dev/null +++ b/pdns/comment.hh @@ -0,0 +1,40 @@ +/* + PowerDNS Versatile Database Driven Nameserver + Copyright (C) 2002 - 2014 PowerDNS.COM BV + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2 + as published by the Free Software Foundation + + Additionally, the license of this program contains a special + exception which allows to distribute the program in binary form when + it is linked against OpenSSL. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#pragma once +#include "utility.hh" +#include "qtype.hh" +#include + +class Comment +{ +public: + Comment() : domain_id(0), modified_at(0) {}; + ~Comment() {}; + + // data + int domain_id; + string qname; //!< the name of the associated RRset, for example: www.powerdns.com + QType qtype; //!< qtype of the associated RRset, ie A, CNAME, MX etc + time_t modified_at; + string account; //!< account last updating this comment + string content; //!< The actual comment. Example: blah blah +}; diff --git a/pdns/dnsbackend.hh b/pdns/dnsbackend.hh index 2b4fae59b7..0bfbb45a1a 100644 --- a/pdns/dnsbackend.hh +++ b/pdns/dnsbackend.hh @@ -39,6 +39,7 @@ class DNSPacket; #include "dns.hh" #include #include "namespaces.hh" +#include "comment.hh" class DNSBackend; struct DomainInfo @@ -206,6 +207,26 @@ public: // end DNSSEC + // comments support + virtual bool listComments(uint32_t domain_id) + { + return false; // unsupported by this backend + } + + virtual bool getComment(Comment& comment) + { + return false; + } + + virtual void feedComment(const Comment& comment) + { + } + + virtual bool replaceComments(const uint32_t domain_id, const string& qname, const QType& qt, const vector& comments) + { + return false; + } + //! returns true if master ip is master for domain name. virtual bool isMaster(const string &name, const string &ip) { diff --git a/pdns/docs/pdns.xml b/pdns/docs/pdns.xml index 743c261086..4d99f5fd06 100644 --- a/pdns/docs/pdns.xml +++ b/pdns/docs/pdns.xml @@ -17314,6 +17314,8 @@ This setting will make PowerDNS renotify the slaves after an AXFR is *received* AutoserialNo CaseDepends DNSSECPartial, no delegation, no key storage + Disabled dataNo + CommentsNo Module namepipe Launch namepipe @@ -17635,6 +17637,8 @@ authoritative). AutoserialNo CaseDepends DNSSECYes, no key storage + Disabled dataNo + CommentsNo Module namebuilt in Launch namerandom @@ -17684,6 +17688,7 @@ authoritative). CaseAll lower DNSSECYes (set gmysql-dnssec or gpgsql-dnssec) Disabled dataYes (v3.4 and up) + CommentsYes (v3.4 and up) Module name < 2.9.3pgmysql Module name > 2.9.2gmysql and gpgsql Launch namegmysql and gpgsql2 and gpgsql @@ -18070,6 +18075,49 @@ insert into domains (id,name,type) values (domains_id_sequence.nextval,'example. + Comments queries + + For listing/modifying comments. For defaults, please see pdns_server --load=BACKEND --config. + + + list-comments-query + + + Called to get all comments in a zone. + Returns fields: domain_id, name, type, modified_at, account, comment. + + + + + insert-comment-query + + + Called to create a single comment for a specific RRSet. + Given fields: domain_id, name, type, modified_at, account, comment + + + + + delete-comment-rrset-query + + + Called to delete all comments for a specific RRset. + Given fields: domain_id, name, type + + + + + delete-comments-query + + + Called to delete all comments for a zone. Usually called before deleting the entire zone. + Given fields: domain_id + + + + + + Fancy records Fancy records are unsupported as of version 3.0 @@ -18282,6 +18330,7 @@ insert into domains (id,name,type) values (domains_id_sequence.nextval,'example. SuperslaveYes AutoserialYes DNSSECYes + CommentsNo Module nameoracle Launch nameoracle @@ -19222,6 +19271,8 @@ VALUES (:zoneid, :ip) SlaveYes SuperslaveYes DNSSECgsqlite3 only (set gsqlite3-dnssec) + Disabled datagsqlite3 only + Commentsgsqlite3 only Module namegsqlite and gsqlite3 Launch namegsqlite and gsqlite3 @@ -19317,6 +19368,8 @@ VALUES (:zoneid, :ip) SuperslaveNo AutoserialYes DNSSECNo + Disabled dataNo + CommentsNo Module namedb2 Launch namedb2 @@ -19408,6 +19461,8 @@ VALUES (:zoneid, :ip) SuperslaveExperimental AutoserialNo DNSSECYes, but no key storage + Disabled dataNo + CommentsNo Module namenone (built in) Launchbind diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 06678def02..051100fdd5 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -29,6 +29,7 @@ #include "misc.hh" #include "arguments.hh" #include "dns.hh" +#include "comment.hh" #include "ueberbackend.hh" #include #include @@ -306,6 +307,7 @@ static void fillZone(const string& zonename, HttpResponse* resp) { doc.AddMember("notified_serial", di.notified_serial, doc.GetAllocator()); doc.AddMember("last_check", (unsigned int) di.last_check, doc.GetAllocator()); + // fill records DNSResourceRecord rr; Value records; records.SetArray(); @@ -329,6 +331,27 @@ static void fillZone(const string& zonename, HttpResponse* resp) { } doc.AddMember("records", records, doc.GetAllocator()); + // fill comments + Comment comment; + Value comments; + comments.SetArray(); + di.backend->listComments(di.id); + while(di.backend->getComment(comment)) { + Value object; + object.SetObject(); + Value jname(comment.qname.c_str(), doc.GetAllocator()); // copy + object.AddMember("name", jname, doc.GetAllocator()); + Value jtype(comment.qtype.getName().c_str(), doc.GetAllocator()); // copy + object.AddMember("type", jtype, doc.GetAllocator()); + object.AddMember("modified_at", comment.modified_at, doc.GetAllocator()); + Value jaccount(comment.account.c_str(), doc.GetAllocator()); // copy + object.AddMember("account", jaccount, doc.GetAllocator()); + Value jcontent(comment.content.c_str(), doc.GetAllocator()); // copy + object.AddMember("content", jcontent, doc.GetAllocator()); + comments.PushBack(object, doc.GetAllocator()); + } + doc.AddMember("comments", comments, doc.GetAllocator()); + resp->setBody(doc); } @@ -343,6 +366,8 @@ void productServerStatisticsFetch(map& out) out["uptime"] = lexical_cast(time(0) - s_starttime); } +static void apiServerZoneRRset(HttpRequest* req, HttpResponse* resp); + static void apiServerZones(HttpRequest* req, HttpResponse* resp) { UeberBackend B; if (req->method == "POST") { @@ -495,12 +520,15 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp) { // empty body on success resp->body = ""; return; + } else if (req->method == "PATCH") { + apiServerZoneRRset(req, resp); + return; + } else if (req->method == "GET") { + fillZone(zonename, resp); + return; } - if(req->method != "GET") - throw HttpMethodNotAllowedException(); - - fillZone(zonename, resp); + throw HttpMethodNotAllowedException(); } static void apiServerZoneRRset(HttpRequest* req, HttpResponse* resp) { @@ -527,44 +555,86 @@ static void apiServerZoneRRset(HttpRequest* req, HttpResponse* resp) { throw ApiException("RRset "+qname+" IN "+qtype.getName()+": Name is out of zone"); if (changetype == "DELETE") { - // delete all matching qname/qtype RRs - di.backend->replaceRRSet(di.id, qname, qtype, vector()); + // delete all matching qname/qtype RRs (and, implictly comments). + if (!di.backend->replaceRRSet(di.id, qname, qtype, vector())) { + throw ApiException("Hosting backend does not support editing records."); + } } else if (changetype == "REPLACE") { + vector new_records; + vector new_comments; + bool replace_records = false; + bool replace_comments = false; + + // gather records DNSResourceRecord rr; - vector rrset; const Value& records = document["records"]; - for(SizeType idx = 0; idx < records.Size(); ++idx) { - const Value& record = records[idx]; - rr.qname = stringFromJson(record, "name"); - rr.content = stringFromJson(record, "content"); - rr.qtype = stringFromJson(record, "type"); - rr.domain_id = di.id; - rr.auth = 1; - rr.ttl = intFromJson(record, "ttl"); - rr.priority = intFromJson(record, "priority"); - rr.disabled = boolFromJson(record, "disabled"); - - rrset.push_back(rr); + if (records.IsArray()) { + replace_records = true; + for(SizeType idx = 0; idx < records.Size(); ++idx) { + const Value& record = records[idx]; + rr.qname = stringFromJson(record, "name"); + rr.content = stringFromJson(record, "content"); + rr.qtype = stringFromJson(record, "type"); + rr.domain_id = di.id; + rr.auth = 1; + rr.ttl = intFromJson(record, "ttl"); + rr.priority = intFromJson(record, "priority"); + rr.disabled = boolFromJson(record, "disabled"); + + if(rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV) + rr.content = lexical_cast(rr.priority)+" "+rr.content; + + if(rr.qname != qname || rr.qtype != qtype) + throw ApiException("Record "+rr.qname+" IN "+rr.qtype.getName()+" "+rr.content+": Record bundled with wrong RRset"); + + try { + shared_ptr drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, rr.content)); + string tmp = drc->serialize(rr.qname); + } + catch(std::exception& e) + { + throw ApiException("Record "+rr.qname+" IN "+rr.qtype.getName()+" "+rr.content+": "+e.what()); + } + + new_records.push_back(rr); + } + } - if(rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV) - rr.content = lexical_cast(rr.priority)+" "+rr.content; + // gather comments + Comment c; + c.domain_id = di.id; + c.qname = qname; + c.qtype = qtype; + time_t now = time(0); + const Value& comments = document["comments"]; + if (comments.IsArray()) { + replace_comments = true; + for(SizeType idx = 0; idx < comments.Size(); ++idx) { + const Value& comment = comments[idx]; + c.modified_at = intFromJson(comment, "modified_at", now); + c.content = stringFromJson(comment, "content"); + c.account = stringFromJson(comment, "account"); + new_comments.push_back(c); + } + } - if(rr.qname != qname || rr.qtype != qtype) - throw ApiException("Record "+rr.qname+" IN "+rr.qtype.getName()+" "+rr.content+": Record bundled with wrong RRset"); + if (!replace_records && !replace_comments) { + throw ApiException("No change"); + } - try { - shared_ptr drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, rr.content)); - string tmp = drc->serialize(rr.qname); + // Actually store the change(s). + di.backend->startTransaction(qname); + if (replace_records) { + if (!di.backend->replaceRRSet(di.id, qname, qtype, new_records)) { + throw ApiException("Hosting backend does not support editing records."); } - catch(std::exception& e) - { - throw ApiException("Record "+rr.qname+" IN "+rr.qtype.getName()+" "+rr.content+": "+e.what()); + } + if (replace_comments) { + if (!di.backend->replaceComments(di.id, qname, qtype, new_comments)) { + throw ApiException("Hosting backend does not support editing comments."); } } - // Actually store the change. - di.backend->startTransaction(qname); - di.backend->replaceRRSet(di.id, qname, qtype, rrset); di.backend->commitTransaction(); } else diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index 8c27bc02a4..f257acb835 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -48,6 +48,7 @@ class AuthZones(ApiTestCase): self.assertIn(k, data) if k in payload: self.assertEquals(data[k], payload[k]) + self.assertEquals(data['comments'], []) def test_CreateZoneWithSymbols(self): payload, data = self.create_zone(name='foo/bar.'+unique_zone_name()) @@ -144,10 +145,7 @@ class AuthZones(ApiTestCase): headers={'content-type': 'application/json'}) self.assertSuccessJson(r) # verify that (only) the new record is there - r = self.session.get( - self.url("/servers/localhost/zones/" + name), - data=json.dumps(payload), - headers={'content-type': 'application/json'}) + r = self.session.get(self.url("/servers/localhost/zones/" + name)) data = r.json()['records'] recs = [rec for rec in data if rec['type'] == payload['type'] and rec['name'] == payload['name']] self.assertEquals(recs, payload['records']) @@ -167,10 +165,7 @@ class AuthZones(ApiTestCase): headers={'content-type': 'application/json'}) self.assertSuccessJson(r) # verify that the records are gone - r = self.session.get( - self.url("/servers/localhost/zones/" + name), - data=json.dumps(payload), - headers={'content-type': 'application/json'}) + r = self.session.get(self.url("/servers/localhost/zones/" + name)) data = r.json()['records'] recs = [rec for rec in data if rec['type'] == payload['type'] and rec['name'] == payload['name']] self.assertEquals(recs, []) @@ -303,6 +298,113 @@ class AuthZones(ApiTestCase): self.assertEquals(r.status_code, 422) self.assertIn('out of zone', r.json()['error']) + def test_ZoneCommentCreate(self): + payload, zone = self.create_zone() + name = payload['name'] + payload = { + 'changetype': 'replace', + 'name': name, + 'type': 'NS', + 'comments': [ + { + 'account': 'test1', + 'content': 'blah blah', + }, + { + 'account': 'test2', + 'content': 'blah blah bleh', + } + ] + } + r = self.session.patch( + self.url("/servers/localhost/zones/" + name), + data=json.dumps(payload), + headers={'content-type': 'application/json'}) + self.assertSuccessJson(r) + # make sure the comments have been set, and that the NS + # records are still present + r = self.session.get(self.url("/servers/localhost/zones/" + name)) + data = r.json() + print data + self.assertNotEquals([r for r in data['records'] if r['type'] == 'NS'], []) + self.assertNotEquals(data['comments'], []) + # verify that modified_at has been set by pdns + self.assertNotEquals([c for c in data['comments']][0]['modified_at'], 0) + + def test_ZoneCommentDelete(self): + # Test: Delete ONLY comments. + payload, zone = self.create_zone() + name = payload['name'] + payload = { + 'changetype': 'replace', + 'name': name, + 'type': 'NS', + 'comments': [] + } + r = self.session.patch( + self.url("/servers/localhost/zones/" + name), + data=json.dumps(payload), + headers={'content-type': 'application/json'}) + self.assertSuccessJson(r) + # make sure the NS records are still present + r = self.session.get(self.url("/servers/localhost/zones/" + name)) + data = r.json() + print data + self.assertNotEquals([r for r in data['records'] if r['type'] == 'NS'], []) + self.assertEquals(data['comments'], []) + + def test_ZoneCommentStayIntact(self): + # Test if comments on an rrset stay intact if the rrset is replaced + payload, zone = self.create_zone() + name = payload['name'] + # create a comment + payload = { + 'changetype': 'replace', + 'name': name, + 'type': 'NS', + 'comments': [ + { + 'account': 'test1', + 'content': 'oh hi there', + 'modified_at': 1111, + 'name': name, # only for assertEquals, ignored by pdns + 'type': 'NS' # only for assertEquals, ignored by pdns + } + ] + } + r = self.session.patch( + self.url("/servers/localhost/zones/" + name), + data=json.dumps(payload), + headers={'content-type': 'application/json'}) + self.assertSuccessJson(r) + # replace rrset records + payload2 = { + 'changetype': 'replace', + 'name': name, + 'type': 'NS', + 'records': [ + { + "name": name, + "type": "NS", + "priority": 0, + "ttl": 3600, + "content": "ns1.bar.com", + "disabled": False + } + ] + } + r = self.session.patch( + self.url("/servers/localhost/zones/" + name), + data=json.dumps(payload2), + headers={'content-type': 'application/json'}) + self.assertSuccessJson(r) + # make sure the comments still exist + r = self.session.get(self.url("/servers/localhost/zones/" + name)) + data = r.json() + print data + self.assertEquals([r for r in data['records'] if r['type'] == 'NS'], payload2['records']) + self.assertEquals(data['comments'], payload['comments']) + @unittest.skipIf(not isRecursor(), "Not applicable") class RecursorZones(ApiTestCase):