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="")
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);
);
+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,
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="") {
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="")
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);
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.
);
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);
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
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)
{
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) {
d_db->doCommand(metadataQuery);
d_db->doCommand(keysQuery);
}
+ d_db->doCommand(commentsQuery);
d_db->doCommand(domainQuery);
}
catch(SSqlException &e) {
).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);
}
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<Comment>& 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;
+}
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<Comment>& comments);
+
private:
string d_qname;
SSql *d_db;
string d_getAllDomainsQuery;
+ string d_ListCommentsQuery;
+ string d_InsertCommentQuery;
+ string d_DeleteCommentRRsetQuery;
+ string d_DeleteCommentsQuery;
+
protected:
bool d_dnssecQueries;
};
--- /dev/null
+/*
+ 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 <sys/types.h>
+
+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
+};
#include "dns.hh"
#include <vector>
#include "namespaces.hh"
+#include "comment.hh"
class DNSBackend;
struct DomainInfo
// 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<Comment>& comments)
+ {
+ return false;
+ }
+
//! returns true if master ip is master for domain name.
virtual bool isMaster(const string &name, const string &ip)
{
<row><entry>Autoserial</entry><entry>No</entry></row>
<row><entry>Case</entry><entry>Depends</entry></row>
<row><entry>DNSSEC</entry><entry>Partial, no delegation, no key storage</entry></row>
+ <row><entry>Disabled data</entry><entry>No</entry></row>
+ <row><entry>Comments</entry><entry>No</entry></row>
<row><entry>Module name</entry><entry>pipe</entry></row>
<row><entry>Launch name</entry><entry>pipe</entry></row>
</tbody>
<row><entry>Autoserial</entry><entry>No</entry></row>
<row><entry>Case</entry><entry>Depends</entry></row>
<row><entry>DNSSEC</entry><entry>Yes, no key storage</entry></row>
+ <row><entry>Disabled data</entry><entry>No</entry></row>
+ <row><entry>Comments</entry><entry>No</entry></row>
<row><entry>Module name</entry><entry>built in</entry></row>
<row><entry>Launch name</entry><entry>random</entry></row>
</tbody>
<row><entry>Case</entry><entry>All lower</entry></row>
<row><entry>DNSSEC</entry><entry>Yes (set gmysql-dnssec or gpgsql-dnssec)</entry></row>
<row><entry>Disabled data</entry><entry>Yes (v3.4 and up)</entry></row>
+ <row><entry>Comments</entry><entry>Yes (v3.4 and up)</entry></row>
<row><entry>Module name < 2.9.3</entry><entry>pgmysql</entry></row>
<row><entry>Module name > 2.9.2</entry><entry>gmysql and gpgsql</entry></row>
<row><entry>Launch name</entry><entry>gmysql and gpgsql2 and gpgsql</entry></row>
</variablelist>
</para>
</sect2>
+ <sect2><title>Comments queries</title>
+ <para>
+ For listing/modifying comments. For defaults, please see <command>pdns_server --load=BACKEND --config</command>.
+ <variablelist>
+ <varlistentry>
+ <term>list-comments-query</term>
+ <listitem>
+ <para>
+ Called to get all comments in a zone.
+ Returns fields: domain_id, name, type, modified_at, account, comment.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>insert-comment-query</term>
+ <listitem>
+ <para>
+ Called to create a single comment for a specific RRSet.
+ Given fields: domain_id, name, type, modified_at, account, comment
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>delete-comment-rrset-query</term>
+ <listitem>
+ <para>
+ Called to delete all comments for a specific RRset.
+ Given fields: domain_id, name, type
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>delete-comments-query</term>
+ <listitem>
+ <para>
+ Called to delete all comments for a zone. Usually called before deleting the entire zone.
+ Given fields: domain_id
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </sect2>
<sect2><title>Fancy records</title>
<warning><para>Fancy records are unsupported as of version 3.0</para></warning>
<para>
<row><entry>Superslave</entry><entry>Yes</entry></row>
<row><entry>Autoserial</entry><entry>Yes</entry></row>
<row><entry>DNSSEC</entry><entry>Yes</entry></row>
+ <row><entry>Comments</entry><entry>No</entry></row>
<row><entry>Module name</entry><entry>oracle</entry></row>
<row><entry>Launch name</entry><entry>oracle</entry></row>
</tbody>
<row><entry>Slave</entry><entry>Yes</entry></row>
<row><entry>Superslave</entry><entry>Yes</entry></row>
<row><entry>DNSSEC</entry><entry>gsqlite3 only (set gsqlite3-dnssec)</entry></row>
+ <row><entry>Disabled data</entry><entry>gsqlite3 only</entry></row>
+ <row><entry>Comments</entry><entry>gsqlite3 only</entry></row>
<row><entry>Module name</entry><entry>gsqlite and gsqlite3</entry></row>
<row><entry>Launch name</entry><entry>gsqlite and gsqlite3</entry></row>
</tbody>
<row><entry>Superslave</entry><entry>No</entry></row>
<row><entry>Autoserial</entry><entry>Yes</entry></row>
<row><entry>DNSSEC</entry><entry>No</entry></row>
+ <row><entry>Disabled data</entry><entry>No</entry></row>
+ <row><entry>Comments</entry><entry>No</entry></row>
<row><entry>Module name</entry><entry>db2</entry></row>
<row><entry>Launch name</entry><entry>db2</entry></row>
<row><entry>Superslave</entry><entry>Experimental</entry></row>
<row><entry>Autoserial</entry><entry>No</entry></row>
<row><entry>DNSSEC</entry><entry>Yes, but no key storage</entry></row>
+ <row><entry>Disabled data</entry><entry>No</entry></row>
+ <row><entry>Comments</entry><entry>No</entry></row>
<row><entry>Module name</entry><entry>none (built in)</entry></row>
<row><entry>Launch</entry><entry>bind</entry></row>
</tbody>
#include "misc.hh"
#include "arguments.hh"
#include "dns.hh"
+#include "comment.hh"
#include "ueberbackend.hh"
#include <boost/format.hpp>
#include <boost/foreach.hpp>
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();
}
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);
}
out["uptime"] = lexical_cast<string>(time(0) - s_starttime);
}
+static void apiServerZoneRRset(HttpRequest* req, HttpResponse* resp);
+
static void apiServerZones(HttpRequest* req, HttpResponse* resp) {
UeberBackend B;
if (req->method == "POST") {
// 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) {
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<DNSResourceRecord>());
+ // delete all matching qname/qtype RRs (and, implictly comments).
+ if (!di.backend->replaceRRSet(di.id, qname, qtype, vector<DNSResourceRecord>())) {
+ throw ApiException("Hosting backend does not support editing records.");
+ }
}
else if (changetype == "REPLACE") {
+ vector<DNSResourceRecord> new_records;
+ vector<Comment> new_comments;
+ bool replace_records = false;
+ bool replace_comments = false;
+
+ // gather records
DNSResourceRecord rr;
- vector<DNSResourceRecord> 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<string>(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<DNSRecordContent> 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<string>(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<DNSRecordContent> 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
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())
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'])
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, [])
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):