]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Add per-rrset-comments
authorChristian Hofstaedtler <christian@hofstaedtler.name>
Tue, 25 Feb 2014 12:29:32 +0000 (13:29 +0100)
committerChristian Hofstaedtler <christian@hofstaedtler.name>
Wed, 26 Feb 2014 07:15:36 +0000 (08:15 +0100)
16 files changed:
modules/gmysqlbackend/gmysqlbackend.cc
modules/gmysqlbackend/no-dnssec.schema.mysql.sql
modules/goraclebackend/goracle-schema.sql
modules/goraclebackend/goraclebackend.cc
modules/gpgsqlbackend/gpgsqlbackend.cc
modules/gpgsqlbackend/no-dnssec.schema.pgsql.sql
modules/gsqlite3backend/gsqlite3backend.cc
modules/gsqlite3backend/no-dnssec.schema.sqlite3.sql
pdns/Makefile.am
pdns/backends/gsql/gsqlbackend.cc
pdns/backends/gsql/gsqlbackend.hh
pdns/comment.hh [new file with mode: 0644]
pdns/dnsbackend.hh
pdns/docs/pdns.xml
pdns/ws-auth.cc
regression-tests.api/test_Zones.py

index 97e1aa667b5384b17103a95591fcbc54a4aaf18b..159fde7bd6c97204ba8f3828cefbb1e5c03eb0fc 100644 (file)
@@ -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="")
index 9636ad53036de6e766b5f17f8b679a575f194db6..02568aadfda3ece5df66f3592a228a4586d29453 100644 (file)
@@ -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);
index bb902e8669cfd23057ec5adb01cbdec938488650..812a515248fee1cdf01c14bc4ec6e9421f0576d5 100644 (file)
@@ -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,
index 4b629ef5f6f08d54d9fa5c6ccc909be39a0fabc8..5a21a1f532e40f9b3381805bda467f145fe278e5 100644 (file)
@@ -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="") {
index 92e7c111a094a3b3108376530fbeef4a762ea6a7..c9667cbffa158cf8941fd85eed57833fbccab518 100644 (file)
@@ -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="")
index 9f9560cbed478ee56841f14feea9834e2a6aa824..e41e363492252fc5d48ad46cd04eef4d5faac7f5 100644 (file)
@@ -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);
index 0e0bb34822d06d18ac07b1ff637b3b0d756d3086..b602efae3bc360ea0a4fdff63dbbce63a2fdf992 100644 (file)
@@ -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.
index 0abcccb32b2b34f4801406ac2d7d839821bbf908..fced5c7aa0f6462f3cbe47f3a0d128e784e4ac39 100644 (file)
@@ -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);
index 67b0f2e2b26c6443fe04b518e7cbfbe5de4f3219..92a79c9c98229137ef4b32370c758093a9387d20 100644 (file)
@@ -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
index 164c819dc681f4892126315a8521c589661e78cc..ed3b32a140c46ed4d24b20372e9800bf618217cc 100644 (file)
@@ -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<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;
+}
index c28d0f43902d69044bff9b2000b63ab1cdab37b2..9b86cb5d749354c9b85a1181a1bf2f791068cdf9 100644 (file)
@@ -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<Comment>& 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 (file)
index 0000000..1587d8b
--- /dev/null
@@ -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 <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
+};
index 2b4fae59b75b5fc90455f68758a8daf72905fc69..0bfbb45a1a7e4e5f436832ffa182efab405ad54a 100644 (file)
@@ -39,6 +39,7 @@ class DNSPacket;
 #include "dns.hh"
 #include <vector>
 #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<Comment>& comments)
+  {
+    return false;
+  }
+
   //! returns true if master ip is master for domain name.
   virtual bool isMaster(const string &name, const string &ip)
   {
index 743c26108662d84971160b3775e66edb5e597174..4d99f5fd067f6a8481bf13f102c567c785a4583d 100644 (file)
@@ -17314,6 +17314,8 @@ This setting will make PowerDNS renotify the slaves after an AXFR is *received*
              <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>
@@ -17635,6 +17637,8 @@ authoritative).
              <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>
@@ -17684,6 +17688,7 @@ authoritative).
              <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 &lt; 2.9.3</entry><entry>pgmysql</entry></row>
              <row><entry>Module name &gt; 2.9.2</entry><entry>gmysql and gpgsql</entry></row>
              <row><entry>Launch name</entry><entry>gmysql and gpgsql2 and gpgsql</entry></row>
@@ -18070,6 +18075,49 @@ insert into domains (id,name,type) values (domains_id_sequence.nextval,'example.
          </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>
@@ -18282,6 +18330,7 @@ insert into domains (id,name,type) values (domains_id_sequence.nextval,'example.
               <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>
@@ -19222,6 +19271,8 @@ VALUES (:zoneid, :ip)
                    <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>
@@ -19317,6 +19368,8 @@ VALUES (:zoneid, :ip)
              <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>
 
@@ -19408,6 +19461,8 @@ VALUES (:zoneid, :ip)
              <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>
index 06678def026d841ea92d651d90fbc840488d52bc..051100fdd52ab6752003607bf62a340cca298aff 100644 (file)
@@ -29,6 +29,7 @@
 #include "misc.hh"
 #include "arguments.hh"
 #include "dns.hh"
+#include "comment.hh"
 #include "ueberbackend.hh"
 #include <boost/format.hpp>
 #include <boost/foreach.hpp>
@@ -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<string,string>& out)
   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") {
@@ -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<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
index 8c27bc02a4c48e73b9d969004dae7c7e7d2e1a20..f257acb83548fc289b03353e514c4c7eaea7a7de 100644 (file)
@@ -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):