Permalink
Browse files

JSON API: provide flush-cache, notify, axfr-receive

pdnscontrol used to send pdns/rec-control commands for those through
the jsonstat command tunnel, but jsonstat (on Auth at least) doesn't
do X-API-Key, so that functionality was broken.

Also removes jsonstat from Auth completely.

Cherry-pick conflicts:
	pdns/ws-recursor.cc (kept jsonstat as is)
  • Loading branch information...
1 parent 7cbcd32 commit f6487526a80d337a75c5f125bd1e0fa4a5cefbf5 @zeha zeha committed Jan 25, 2015
Showing with 139 additions and 63 deletions.
  1. +6 −3 pdns/docs/httpapi/api_spec.md
  2. +32 −0 pdns/json.cc
  3. +1 −0 pdns/json.hh
  4. +54 −60 pdns/ws-auth.cc
  5. +14 −0 pdns/ws-recursor.cc
  6. +13 −0 regression-tests.api/test_Servers.py
  7. +19 −0 regression-tests.api/test_Zones.py
@@ -49,6 +49,12 @@ For interactions that do not directly map onto CRUD, we use these:
* GET: Query. Success reply: `200 OK`
* PUT: Action/Execute. Success reply: `200 OK`
+Action/Execute methods return a JSON body of this format:
+
+ {
+ "message": "result message"
+ }
+
Authentication
--------------
@@ -477,8 +483,6 @@ Not supported for recursors.
Clients MUST NOT send a body.
-**TODO**: Not yet implemented.
-
URL: /servers/:server\_id/zones/:zone\_id/axfr-retrieve
-------------------------------------------------------
@@ -494,7 +498,6 @@ Not supported for recursors.
Clients MUST NOT send a body.
-**TODO**: Not yet implemented.
URL: /servers/:server\_id/zones/:zone\_id/check
-----------------------------------------------
View
@@ -1,3 +1,24 @@
+/*
+ Copyright (C) 2002 - 2015 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
+*/
+
#include "json.hh"
#include "namespaces.hh"
#include "misc.hh"
@@ -119,3 +140,14 @@ string returnJsonError(const string& error)
doc.AddMember("error", jerror, doc.GetAllocator());
return makeStringFromDocument(doc);
}
+
+/* success response */
+string returnJsonMessage(const string& message)
+{
+ Document doc;
+ doc.SetObject();
+ Value jmessage;
+ jmessage.SetString(message.c_str());
+ doc.AddMember("result", jmessage, doc.GetAllocator());
+ return makeStringFromDocument(doc);
+}
View
@@ -28,6 +28,7 @@
std::string returnJsonObject(const std::map<std::string, std::string>& items);
std::string returnJsonError(const std::string& error);
+std::string returnJsonMessage(const std::string& message);
std::string makeStringFromDocument(const rapidjson::Document& doc);
int intFromJson(const rapidjson::Value& container, const char* key);
int intFromJson(const rapidjson::Value& container, const char* key, const int default_value);
View
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2002 - 2014 PowerDNS.COM BV
+ Copyright (C) 2002 - 2015 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
@@ -42,6 +42,7 @@
#include "dnsseckeeper.hh"
#include <iomanip>
#include "zoneparser-tng.hh"
+#include "common_startup.hh"
#ifdef HAVE_CONFIG_H
# include <config.h>
@@ -878,6 +879,42 @@ static void apiServerZoneExport(HttpRequest* req, HttpResponse* resp) {
}
}
+static void apiServerZoneAxfrRetrieve(HttpRequest* req, HttpResponse* resp) {
+ string zonename = apiZoneIdToName(req->parameters["id"]);
+
+ if(req->method != "PUT")
+ throw HttpMethodNotAllowedException();
+
+ UeberBackend B;
+ DomainInfo di;
+ if(!B.getDomainInfo(zonename, di))
+ throw ApiException("Could not find domain '"+zonename+"'");
+
+ if(di.masters.empty())
+ throw ApiException("Domain '"+zonename+"' is not a slave domain (or has no master defined)");
+
+ random_shuffle(di.masters.begin(), di.masters.end());
+ Communicator.addSuckRequest(zonename, di.masters.front());
+ resp->body = returnJsonMessage("Added retrieval request for '"+zonename+"' from master "+di.masters.front());
+}
+
+static void apiServerZoneNotify(HttpRequest* req, HttpResponse* resp) {
+ string zonename = apiZoneIdToName(req->parameters["id"]);
+
+ if(req->method != "PUT")
+ throw HttpMethodNotAllowedException();
+
+ UeberBackend B;
+ DomainInfo di;
+ if(!B.getDomainInfo(zonename, di))
+ throw ApiException("Could not find domain '"+zonename+"'");
+
+ if(!Communicator.notifyDomain(zonename))
+ throw ApiException("Failed to add to the queue - see server log");
+
+ resp->body = returnJsonMessage("Notification queued");
+}
+
static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr) {
if (rr.qtype.getCode() == QType::A) {
uint32_t ip;
@@ -1140,65 +1177,21 @@ static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
resp->setBody(doc);
}
-void AuthWebServer::jsonstat(HttpRequest* req, HttpResponse* resp)
-{
- string command;
-
- if(req->getvars.count("command")) {
- command = req->getvars["command"];
- req->getvars.erase("command");
- }
+void apiServerFlushCache(HttpRequest* req, HttpResponse* resp) {
+ if(req->method != "PUT")
+ throw HttpMethodNotAllowedException();
- if(command == "flush-cache") {
- extern PacketCache PC;
- int number;
- if(req->getvars["domain"].empty())
- number = PC.purge();
- else
- number = PC.purge(req->getvars["domain"]);
-
- map<string, string> object;
- object["number"]=lexical_cast<string>(number);
- //cerr<<"Flushed cache for '"<<parameters["domain"]<<"', cleaned "<<number<<" records"<<endl;
- resp->body = returnJsonObject(object);
- resp->status = 200;
- return;
- }
- else if(command == "pdns-control") {
- if(req->method!="POST")
- throw HttpMethodNotAllowedException();
- // cout<<"post: "<<post<<endl;
- rapidjson::Document document;
- req->json(document);
- // cout<<"Parameters: '"<<document["parameters"].GetString()<<"'\n";
- vector<string> parameters;
- stringtok(parameters, document["parameters"].GetString(), " \t");
-
- DynListener::g_funk_t* ptr=0;
- if(!parameters.empty())
- ptr = DynListener::getFunc(toUpper(parameters[0]));
- map<string, string> m;
-
- if(ptr) {
- resp->status = 200;
- m["result"] = (*ptr)(parameters, 0);
- } else {
- resp->status = 404;
- m["error"]="No such function "+toUpper(parameters[0]);
- }
- resp->body = returnJsonObject(m);
- return;
- }
- else if(command=="log-grep") {
- // legacy parameter name hack
- req->getvars["q"] = req->getvars["needle"];
- apiServerSearchLog(req, resp);
- return;
- }
+ extern PacketCache PC;
+ int count;
+ if(req->getvars["domain"].empty())
+ count = PC.purge();
+ else
+ count = PC.purge(req->getvars["domain"]);
- resp->body = returnJsonError("No or unknown command given");
- resp->status = 404;
- return;
+ map<string, string> object;
+ object["count"] = lexical_cast<string>(count);
+ object["result"] = "Flushed cache.";
+ resp->body = returnJsonObject(object);
}
void AuthWebServer::cssfunction(HttpRequest* req, HttpResponse* resp)
@@ -1242,18 +1235,19 @@ void AuthWebServer::webThread()
try {
if(::arg().mustDo("experimental-json-interface")) {
d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig);
+ d_ws->registerApiHandler("/servers/localhost/flush-cache", &apiServerFlushCache);
d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog);
d_ws->registerApiHandler("/servers/localhost/search-data", &apiServerSearchData);
d_ws->registerApiHandler("/servers/localhost/statistics", &apiServerStatistics);
+ d_ws->registerApiHandler("/servers/localhost/zones/<id>/axfr-retrieve", &apiServerZoneAxfrRetrieve);
d_ws->registerApiHandler("/servers/localhost/zones/<id>/cryptokeys/<key_id>", &apiZoneCryptokeys);
d_ws->registerApiHandler("/servers/localhost/zones/<id>/cryptokeys", &apiZoneCryptokeys);
d_ws->registerApiHandler("/servers/localhost/zones/<id>/export", &apiServerZoneExport);
+ d_ws->registerApiHandler("/servers/localhost/zones/<id>/notify", &apiServerZoneNotify);
d_ws->registerApiHandler("/servers/localhost/zones/<id>", &apiServerZoneDetail);
d_ws->registerApiHandler("/servers/localhost/zones", &apiServerZones);
d_ws->registerApiHandler("/servers/localhost", &apiServerDetail);
d_ws->registerApiHandler("/servers", &apiServer);
- // legacy dispatch
- d_ws->registerApiHandler("/jsonstat", boost::bind(&AuthWebServer::jsonstat, this, _1, _2));
}
d_ws->registerWebHandler("/style.css", boost::bind(&AuthWebServer::cssfunction, this, _1, _2));
d_ws->registerWebHandler("/", boost::bind(&AuthWebServer::indexfunction, this, _1, _2));
View
@@ -417,6 +417,19 @@ static void apiServerSearchData(HttpRequest* req, HttpResponse* resp) {
resp->setBody(doc);
}
+static void apiServerFlushCache(HttpRequest* req, HttpResponse* resp) {
+ if(req->method != "PUT")
+ throw HttpMethodNotAllowedException();
+
+ string canon = toCanonic("", req->getvars["domain"]);
+ int count = broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeCache, canon));
+ count += broadcastAccFunction<uint64_t>(boost::bind(pleaseWipeAndCountNegCache, canon));
+ map<string, string> object;
+ object["count"] = lexical_cast<string>(count);
+ object["result"] = "Flushed cache.";
+ resp->body = returnJsonObject(object);
+}
+
RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
{
RecursorControlParser rcp; // inits
@@ -426,6 +439,7 @@ RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
// legacy dispatch
d_ws->registerApiHandler("/jsonstat", boost::bind(&RecursorWebServer::jsonstat, this, _1, _2));
+ d_ws->registerApiHandler("/servers/localhost/flush-cache", &apiServerFlushCache);
d_ws->registerApiHandler("/servers/localhost/config/allow-from", &apiServerConfigAllowFrom);
d_ws->registerApiHandler("/servers/localhost/config", &apiServerConfig);
d_ws->registerApiHandler("/servers/localhost/search-log", &apiServerSearchLog);
@@ -41,3 +41,16 @@ def test_read_statistics(self):
self.assert_success_json(r)
data = dict([(r['name'], r['value']) for r in r.json()])
self.assertIn('uptime', data)
+
+ def test_flush_cache(self):
+ r = self.session.put(self.url("/servers/localhost/flush-cache?domain=example.org."))
+ self.assert_success_json(r)
+ data = r.json()
+ self.assertIn('count', data)
+
+ def test_flush_complete_cache(self):
+ r = self.session.put(self.url("/servers/localhost/flush-cache"))
+ self.assert_success_json(r)
+ data = r.json()
+ self.assertIn('count', data)
+ self.assertEqual(data['result'], 'Flushed cache.')
@@ -180,6 +180,25 @@ def test_delete_slave_zone(self):
r = self.session.delete(self.url("/servers/localhost/zones/" + data['id']))
r.raise_for_status()
+ def test_retrieve_slave_zone(self):
+ payload, data = self.create_zone(kind='Slave', nameservers=None, masters=['127.0.0.2'])
+ print "payload:", payload
+ print "data:", data
+ r = self.session.put(self.url("/servers/localhost/zones/" + data['id'] + "/axfr-retrieve"))
+ data = r.json()
+ print "status for axfr-retrieve:", data
+ self.assertEqual(data['result'], u'Added retrieval request for \'' + payload['name'] +
+ '\' from master 127.0.0.2')
+
+ def test_notify_master_zone(self):
+ payload, data = self.create_zone(kind='Master')
+ print "payload:", payload
+ print "data:", data
+ r = self.session.put(self.url("/servers/localhost/zones/" + data['id'] + "/notify"))
+ data = r.json()
+ print "status for notify:", data
+ self.assertEqual(data['result'], 'Notification queued')
+
def test_get_zone_with_symbols(self):
payload, data = self.create_zone(name='foo/bar.'+unique_zone_name())
name = payload['name']

0 comments on commit f648752

Please sign in to comment.