From: Christian Hofstaedtler Date: Mon, 28 Apr 2014 12:36:50 +0000 (+0200) Subject: API: Support adding records/comments at zone creation X-Git-Tag: rec-3.6.0-rc1~37^2~1 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=f63168e6cc69877147736bff81457d7cdc25bc88;p=pdns API: Support adding records/comments at zone creation Records and comments can now be supplied at zone creation time; a given SOA record replaces the automatically generated SOA. If no nameservers are given, we assume the user supplied NS records as well. (This now allows creating a zone without NS records, which may be useful for slaves.) Default SOA serial is now 0 to match other parts of PowerDNS. SOA-EDIT-API is now honored at creation time. Fixes #1379. Fixes #1376. --- diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 1d6dc1bc9..851cb8fd3 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -50,6 +50,9 @@ using namespace rapidjson; extern StatBag S; +static void patchZone(HttpRequest* req, HttpResponse* resp); +static void makePtr(const DNSResourceRecord& rr, DNSResourceRecord* ptr); + AuthWebServer::AuthWebServer() { d_start=time(0); @@ -372,7 +375,78 @@ void productServerStatisticsFetch(map& out) out["uptime"] = lexical_cast(time(0) - s_starttime); } -static void patchZone(HttpRequest* req, HttpResponse* resp); +static void gatherRecords(const Value& container, vector& new_records, vector& new_ptrs) { + UeberBackend B; + DNSResourceRecord rr; + const Value& records = container["records"]; + if (records.IsArray()) { + for (SizeType idx = 0; idx < records.Size(); ++idx) { + const Value& record = records[idx]; + rr.qname = stringFromJson(record, "name"); + rr.qtype = stringFromJson(record, "type"); + rr.content = stringFromJson(record, "content"); + rr.auth = 1; + rr.ttl = intFromJson(record, "ttl"); + rr.priority = intFromJson(record, "priority"); + rr.disabled = boolFromJson(record, "disabled"); + + string temp_content = rr.content; + if (rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV) + temp_content = lexical_cast(rr.priority)+" "+rr.content; + + try { + shared_ptr drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, temp_content)); + string tmp = drc->serialize(rr.qname); + } + catch(std::exception& e) + { + throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": "+e.what()); + } + + if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) && + boolFromJson(record, "set-ptr", false) == true) { + DNSResourceRecord ptr; + makePtr(rr, &ptr); + + // verify that there's a zone for the PTR + DNSPacket fakePacket; + SOAData sd; + fakePacket.qtype = QType::PTR; + if (!B.getAuth(&fakePacket, &sd, ptr.qname, 0)) + throw ApiException("Could not find domain for PTR '"+ptr.qname+"' requested for '"+ptr.content+"'"); + + ptr.domain_id = sd.domain_id; + new_ptrs.push_back(ptr); + } + + new_records.push_back(rr); + } + } +} + +static void gatherComments(const Value& container, vector& new_comments, bool use_name_type_from_container) { + Comment c; + if (use_name_type_from_container) { + c.qname = stringFromJson(container, "name"); + c.qtype = stringFromJson(container, "type"); + } + + time_t now = time(0); + const Value& comments = container["comments"]; + if (comments.IsArray()) { + for(SizeType idx = 0; idx < comments.Size(); ++idx) { + const Value& comment = comments[idx]; + if (!use_name_type_from_container) { + c.qname = stringFromJson(comment, "name"); + c.qtype = stringFromJson(comment, "type"); + } + c.modified_at = intFromJson(comment, "modified_at", now); + c.content = stringFromJson(comment, "content"); + c.account = stringFromJson(comment, "account"); + new_comments.push_back(c); + } + } +} static void updateDomainSettingsFromDocument(const DomainInfo& di, const string& zonename, Document& document) { string master; @@ -399,6 +473,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) { Document document; req->json(document); string zonename = stringFromJson(document, "name"); + string dotsuffix = "." + zonename; // TODO: better validation of zonename if(zonename.empty()) throw ApiException("Zone name empty"); @@ -416,56 +491,94 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp) { stringFromJson(document, "kind"); const Value &nameservers = document["nameservers"]; - if (!nameservers.IsArray() || nameservers.Size() == 0) - throw ApiException("Need at least one nameserver"); + if (!nameservers.IsArray()) + throw ApiException("Nameservers list must be given (but can be empty if NS records are supplied)"); - for (SizeType i = 0; i < nameservers.Size(); ++i) { - if (!nameservers[i].IsString()) { - throw ApiException("Nameservers must be strings."); - } - } + string soa_edit_api_kind; + if (document["soa_edit_api"].IsString()) + soa_edit_api_kind = document["soa_edit_api"].GetString(); - // no going back after this - if(!B.createDomain(zonename)) - throw ApiException("Creating domain '"+zonename+"' failed"); + // if records/comments are given, load and check them + bool have_soa = false; + vector new_records; + vector new_comments; + vector new_ptrs; + gatherRecords(document, new_records, new_ptrs); + gatherComments(document, new_comments, false); - if(!B.getDomainInfo(zonename, di)) - throw ApiException("Creating domain '"+zonename+"' failed: lookup of domain ID failed"); + DNSResourceRecord rr; - vector rrset; + BOOST_FOREACH(rr, new_records) { + if (!iends_with(rr.qname, dotsuffix) && !pdns_iequals(rr.qname, zonename)) + throw ApiException("RRset "+rr.qname+" IN "+rr.qtype.getName()+": Name is out of zone"); + + if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) { + have_soa = true; + editSOARecord(rr, soa_edit_api_kind); + } + } - // create SOA record so zone "really" exists - DNSResourceRecord rr; rr.qname = zonename; - rr.content = (boost::format("%s hostmaster@%s %d") - % nameservers[SizeType(0)].GetString() - % zonename - % intFromJson(document, "serial", 1) - ).str(); - SOAData sd; - fillSOAData(rr.content, sd); - rr.content = serializeSOAData(sd); - rr.qtype = "SOA"; - rr.domain_id = di.id; rr.auth = 1; - rr.ttl = ::arg().asNum( "default-ttl" ); + rr.ttl = ::arg().asNum("default-ttl"); rr.priority = 0; - rrset.push_back(rr); + if (!have_soa) { + // synthesize a SOA record so the zone "really" exists + + SOAData sd; + sd.qname = zonename; + sd.nameserver = arg()["default-soa-name"]; + if (!arg().isEmpty("default-soa-mail")) { + sd.hostmaster = arg()["default-soa-mail"]; + attodot(sd.hostmaster); + } else { + sd.hostmaster = "hostmaster." + zonename; + } + sd.serial = intFromJson(document, "serial", 0); + sd.ttl = rr.ttl; + sd.refresh = ::arg().asNum("soa-refresh-default"); + sd.retry = ::arg().asNum("soa-retry-default"); + sd.expire = ::arg().asNum("soa-expire-default"); + sd.default_ttl = ::arg().asNum("soa-minimum-ttl"); + + rr.content = serializeSOAData(sd); + rr.qtype = "SOA"; + editSOARecord(rr, soa_edit_api_kind); + new_records.push_back(rr); + } + + // create NS records if nameservers are given for (SizeType i = 0; i < nameservers.Size(); ++i) { + if (!nameservers[i].IsString()) + throw ApiException("Nameservers must be strings"); rr.content = nameservers[i].GetString(); rr.qtype = "NS"; - rrset.push_back(rr); + new_records.push_back(rr); } + // no going back after this + if(!B.createDomain(zonename)) + throw ApiException("Creating domain '"+zonename+"' failed"); + + if(!B.getDomainInfo(zonename, di)) + throw ApiException("Creating domain '"+zonename+"' failed: lookup of domain ID failed"); + di.backend->startTransaction(zonename, di.id); - BOOST_FOREACH(rr, rrset) { + + BOOST_FOREACH(rr, new_records) { + rr.domain_id = di.id; di.backend->feedRecord(rr); } - di.backend->commitTransaction(); + BOOST_FOREACH(Comment& c, new_comments) { + c.domain_id = di.id; + di.backend->feedComment(c); + } updateDomainSettingsFromDocument(di, zonename, document); + di.backend->commitTransaction(); + fillZone(zonename, resp); return; } @@ -594,6 +707,8 @@ static void patchZone(HttpRequest* req, HttpResponse* resp) { throw ApiException("Could not find domain '"+zonename+"'"); string dotsuffix = "." + zonename; + vector new_records; + vector new_comments; vector new_ptrs; Document document; @@ -628,85 +743,30 @@ static void patchZone(HttpRequest* req, HttpResponse* resp) { } } else if (changetype == "REPLACE") { - vector new_records; - vector new_comments; - bool replace_records = false; - bool replace_comments = false; - - // gather records - DNSResourceRecord rr; - const Value& records = rrset["records"]; - 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.qname != qname || rr.qtype != qtype) - throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": Record wrongly bundled with RRset " + qname + "/" + qtype.getName()); - - string temp_content = rr.content; - if (rr.qtype.getCode() == QType::MX || rr.qtype.getCode() == QType::SRV) - temp_content = lexical_cast(rr.priority)+" "+rr.content; - - try { - shared_ptr drc(DNSRecordContent::mastermake(rr.qtype.getCode(), 1, temp_content)); - string tmp = drc->serialize(rr.qname); - } - catch(std::exception& e) - { - throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": "+e.what()); - } - - if ((rr.qtype.getCode() == QType::A || rr.qtype.getCode() == QType::AAAA) && - boolFromJson(record, "set-ptr", false) == true) { - DNSResourceRecord ptr; - makePtr(rr, &ptr); - - // verify that there's a zone for the PTR - DNSPacket fakePacket; - SOAData sd; - fakePacket.qtype = QType::PTR; - if (!B.getAuth(&fakePacket, &sd, ptr.qname, 0)) - throw ApiException("Could not find domain for PTR '"+ptr.qname+"' requested for '"+ptr.content+"'"); - - ptr.domain_id = sd.domain_id; - new_ptrs.push_back(ptr); - } - - if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) { - soa_edit_done = editSOARecord(rr, soa_edit_api_kind); - } - - new_records.push_back(rr); + new_records.clear(); + new_comments.clear(); + // new_ptrs is merged + gatherRecords(rrset, new_records, new_ptrs); + gatherComments(rrset, new_comments, true); + + BOOST_FOREACH(DNSResourceRecord& rr, new_records) { + rr.domain_id = di.id; + + if (rr.qname != qname || rr.qtype != qtype) + throw ApiException("Record "+rr.qname+"/"+rr.qtype.getName()+" "+rr.content+": Record wrongly bundled with RRset " + qname + "/" + qtype.getName()); + + if (rr.qtype.getCode() == QType::SOA && pdns_iequals(rr.qname, zonename)) { + soa_edit_done = editSOARecord(rr, soa_edit_api_kind); } } - // gather comments - Comment c; - c.domain_id = di.id; - c.qname = qname; - c.qtype = qtype; - time_t now = time(0); - const Value& comments = rrset["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); - } + BOOST_FOREACH(Comment& c, new_comments) { + c.domain_id = di.id; } + bool replace_records = rrset["records"].IsArray(); + bool replace_comments = rrset["comments"].IsArray(); + if (!replace_records && !replace_comments) { throw ApiException("No change for RRset " + qname + "/" + qtype.getName()); } diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index e8ea4f6d2..bc2551432 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -45,20 +45,77 @@ class AuthZones(ApiTestCase): return (payload, r.json()) def test_CreateZone(self): - payload, data = self.create_zone() + payload, data = self.create_zone(serial=22) for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api'): self.assertIn(k, data) if k in payload: self.assertEquals(data[k], payload[k]) self.assertEquals(data['comments'], []) + # validate generated SOA + self.assertEquals( + [r['content'] for r in data['records'] if r['type'] == 'SOA'][0], + "a.misconfigured.powerdns.server hostmaster."+payload['name']+" "+str(payload['serial'])+" 10800 3600 604800 3600" + ) def test_CreateZoneWithSoaEditApi(self): - payload, data = self.create_zone(soa_edit_api='EPOCH') - for k in ('id', 'url', 'name', 'masters', 'kind', 'last_check', 'notified_serial', 'serial', 'soa_edit_api'): + # soa_edit_api wins over serial + payload, data = self.create_zone(soa_edit_api='EPOCH', serial=10) + for k in ('soa_edit_api', ): self.assertIn(k, data) if k in payload: self.assertEquals(data[k], payload[k]) - self.assertEquals(data['comments'], []) + # generated EPOCH serial surely is > fixed serial we passed in + print data + self.assertGreater(data['serial'], payload['serial']) + soa_serial = int([r['content'].split(' ')[2] for r in data['records'] if r['type'] == 'SOA'][0]) + self.assertGreater(soa_serial, payload['serial']) + self.assertEquals(soa_serial, data['serial']) + + def test_CreateZoneWithRecords(self): + name = unique_zone_name() + records = [ + { + "name": name, + "type": "A", + "priority": 0, + "ttl": 3600, + "content": "4.3.2.1", + "disabled": False + } + ] + payload, data = self.create_zone(name=name, records=records) + # check our record has appeared + self.assertEquals([r for r in data['records'] if r['type'] == records[0]['type']], records) + + def test_CreateZoneWithComments(self): + name = unique_zone_name() + comments = [ + { + 'name': name, + 'type': 'SOA', + 'account': 'test1', + 'content': 'blah blah', + 'modified_at': 11112, + } + ] + payload, data = self.create_zone(name=name, comments=comments) + # check our comment has appeared + self.assertEquals(data['comments'], comments) + + def test_CreateZoneWithCustomSOA(self): + name = unique_zone_name() + records = [ + { + "name": name, + "type": "SOA", + "priority": 0, + "ttl": 3600, + "content": "ns1.example.net testmaster@example.net 10 10800 3600 604800 3600", + "disabled": False + } + ] + payload, data = self.create_zone(name=name, records=records) + self.assertEquals([r for r in data['records'] if r['type'] == records[0]['type']], records) def test_CreateZoneTrailingDot(self): # Trailing dots should not end up in the zone name.