src/knot/zone/measure.h
src/knot/zone/node.c
src/knot/zone/node.h
+src/knot/zone/redis.c
+src/knot/zone/redis.h
src/knot/zone/reverse.c
src/knot/zone/reverse.h
src/knot/zone/semantic-check.c
knot/zone/measure.c \
knot/zone/node.c \
knot/zone/node.h \
+ knot/zone/redis.c \
+ knot/zone/redis.h \
knot/zone/reverse.c \
knot/zone/reverse.h \
knot/zone/semantic-check.c \
#include "knot/zone/adds_tree.h"
#include "knot/zone/adjust.h"
#include "knot/zone/digest.h"
+#include "knot/zone/redis.h"
#include "knot/zone/serial.h"
#include "knot/zone/zone-diff.h"
#include "knot/zone/zonefile.h"
return ret;
}
+static int redis_wr_rr(const knot_rrset_t *rr, void *ctx)
+{
+ return zone_redis_write_rrset(ctx, rr);
+}
+
+static int redis_wr_node(zone_node_t *node, void *ctx)
+{
+ return zone_redis_write_node(ctx, node);
+}
+
+static int commit_redis(conf_t *conf, zone_update_t *update)
+{
+ uint8_t db_instance = 0;
+ bool db_enabled = conf_zone_rdb_enabled(conf, update->zone->name, false, &db_instance);
+ if (!db_enabled) {
+ return KNOT_EOK;
+ }
+
+ struct redisContext *db_ctx = zone_redis_connect(conf);
+ if (db_ctx == NULL) {
+ return KNOT_ECONN;
+ }
+
+ bool incremental = ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && update->zone->contents != NULL);
+ if (incremental) {
+ zone_redis_err_t err;
+ uint32_t redis_soa = 0;
+ int soa_ret = zone_redis_serial(db_ctx, db_instance, update->zone->name, &redis_soa, err);
+ incremental = (soa_ret == KNOT_EOK && redis_soa == zone_contents_serial(update->zone->contents));
+ }
+
+ zone_redis_txn_t txn;
+ int ret = zone_redis_txn_begin(&txn, db_ctx, db_instance, update->zone->name, incremental);
+ if (ret != KNOT_EOK) {
+ zone_redis_disconnect(db_ctx);
+ return ret;
+ }
+
+ if (incremental) {
+ txn.removals = true;
+ ret = zone_update_foreach(update, false, redis_wr_rr, &txn);
+ if (ret == KNOT_EOK) {
+ txn.removals = false;
+ ret = zone_update_foreach(update, true, redis_wr_rr, &txn);
+ }
+ } else {
+ ret = zone_contents_apply(update->new_cont, redis_wr_node, &txn);
+ if (ret == KNOT_EOK) {
+ ret = zone_contents_nsec3_apply(update->new_cont, redis_wr_node, &txn);
+ }
+ }
+
+ if (ret == KNOT_EOK) {
+ ret = zone_redis_txn_commit(&txn);
+ }
+ if (ret != KNOT_EOK) {
+ if (ret == KNOT_ERDB) {
+ log_zone_error(update->zone->name, "rdb, update aborted (%s)", txn.err);
+ }
+ (void)zone_redis_txn_abort(&txn);
+ } else {
+ log_zone_info(update->zone->name, "database updated, instance %u, serial %u",
+ db_instance, zone_contents_serial(update->new_cont));
+ }
+
+ zone_redis_disconnect(db_ctx);
+ return ret;
+}
+
static int commit_incremental(conf_t *conf, zone_update_t *update)
{
assert(update);
return ret;
}
+ ret = commit_redis(conf, update);
+ if (ret != KNOT_EOK) {
+ log_zone_error(update->zone->name, "zone database update failed (%s)", knot_strerror(ret));
+ discard_adds_tree(update);
+ return ret;
+ }
+
ret = commit_journal(conf, update);
if (ret != KNOT_EOK) {
log_zone_error(update->zone->name, "journal update failed (%s)", knot_strerror(ret));
--- /dev/null
+/* Copyright (C) CZ.NIC, z.s.p.o. and contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * For more information, see <https://www.knot-dns.cz/>
+ */
+
+#include "knot/zone/redis.h"
+
+#include <string.h>
+
+#ifdef ENABLE_REDIS
+
+#include "knot/common/hiredis.h"
+
+struct redisContext *zone_redis_connect(conf_t *conf)
+{
+ return rdb_connect(conf);
+}
+
+void zone_redis_disconnect(struct redisContext *ctx)
+{
+ return rdb_disconnect(ctx);
+}
+
+static int check_reply(struct redisContext *rdb, redisReply *reply,
+ int expected_type, zone_redis_err_t err)
+{
+ if (reply == NULL) {
+ if (rdb->err != REDIS_OK) {
+ strlcpy(err, rdb->errstr, sizeof(zone_redis_err_t));
+ } else {
+ strlcpy(err, "no reply", sizeof(zone_redis_err_t));
+ }
+ return KNOT_ERDB;
+ } else if (reply->type == REDIS_REPLY_ERROR) {
+ strlcpy(err, reply->str, sizeof(zone_redis_err_t));
+ return KNOT_ERDB;
+ } else if (reply->type != expected_type) {
+ strlcpy(err, "unexpected reply", sizeof(zone_redis_err_t));
+ return KNOT_ERDB;
+ } else if (reply->type == REDIS_REPLY_ARRAY && reply->elements == 0) {
+ return KNOT_ENOENT;
+ } else if (reply->type == REDIS_REPLY_STATUS && strcmp(RDB_RETURN_OK, reply->str) != 0) {
+ return KNOT_EACCES;
+ }
+
+ return KNOT_EOK;
+}
+
+int zone_redis_txn_begin(zone_redis_txn_t *txn, struct redisContext *rdb,
+ uint8_t instance, const knot_dname_t *zone_name,
+ bool incremental)
+{
+ if (txn == NULL || rdb == NULL || zone_name == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ txn->rdb = rdb;
+ txn->instance = instance;
+ txn->origin = zone_name;
+ txn->origin_len = knot_dname_size(zone_name);
+ txn->incremental = incremental;
+ txn->removals = false;
+ txn->err[0] = '\0';
+
+ const char *cmd = txn->incremental ? RDB_CMD_UPD_BEGIN " %b %b" :
+ RDB_CMD_ZONE_BEGIN " %b %b";
+
+ redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+ &txn->instance, sizeof(txn->instance));
+ int ret = check_reply(rdb, reply, REDIS_REPLY_STRING, txn->err);
+ if (ret != KNOT_EOK) {
+ freeReplyObject(reply);
+ return ret;
+ }
+ if (reply->len != sizeof(txn->rdb_txn)) {
+ freeReplyObject(reply);
+ return KNOT_EMALF;
+ }
+
+ memcpy(&txn->rdb_txn, reply->str, sizeof(txn->rdb_txn));
+ freeReplyObject(reply);
+
+ return KNOT_EOK;
+}
+
+int zone_redis_write_rrset(zone_redis_txn_t *txn, const knot_rrset_t *rr)
+{
+ if (txn == NULL || rr == NULL || (txn->removals && !txn->incremental)) {
+ return KNOT_EINVAL;
+ }
+
+ const char *cmd = !txn->incremental ? RDB_CMD_ZONE_STORE " %b %b %b %d %d %d %b" :
+ txn->removals ? RDB_CMD_UPD_REMOVE " %b %b %b %d %d %d %b" :
+ RDB_CMD_UPD_ADD " %b %b %b %d %d %d %b";
+
+ redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+ &txn->rdb_txn, sizeof(txn->rdb_txn),
+ rr->owner, knot_dname_size(rr->owner), rr->type, rr->ttl,
+ rr->rrs.count, rr->rrs.rdata, rr->rrs.size);
+ int ret = check_reply(txn->rdb, reply, REDIS_REPLY_STATUS, txn->err);
+ if (ret != KNOT_EOK) {
+ freeReplyObject(reply);
+ return ret;
+ }
+
+ freeReplyObject(reply);
+
+ return KNOT_EOK;
+}
+
+int zone_redis_write_node(zone_redis_txn_t *txn, const zone_node_t *node)
+{
+ if (txn == NULL || node == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = KNOT_EOK;
+ for (uint16_t i = 0; i < node->rrset_count && ret == KNOT_EOK; i++) {
+ knot_rrset_t rrset = node_rrset_at(node, i);
+ ret = zone_redis_write_rrset(txn, &rrset);
+ }
+
+ return ret;
+}
+
+int zone_redis_txn_commit(zone_redis_txn_t *txn)
+{
+ if (txn == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const char *cmd = txn->incremental ? RDB_CMD_UPD_COMMIT " %b %b" :
+ RDB_CMD_ZONE_COMMIT " %b %b";
+
+ redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+ &txn->rdb_txn, sizeof(txn->rdb_txn));
+ int ret = check_reply(txn->rdb, reply, REDIS_REPLY_STATUS, txn->err);
+ if (ret != KNOT_EOK) {
+ freeReplyObject(reply);
+ return ret;
+ }
+
+ memset(txn, 0, sizeof(*txn));
+ freeReplyObject(reply);
+
+ return KNOT_EOK;
+}
+
+int zone_redis_txn_abort(zone_redis_txn_t *txn)
+{
+ if (txn == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const char *cmd = txn->incremental ? RDB_CMD_UPD_ABORT " %b %b" :
+ RDB_CMD_ZONE_ABORT " %b %b";
+
+ redisReply *reply = redisCommand(txn->rdb, cmd, txn->origin, txn->origin_len,
+ &txn->rdb_txn, sizeof(txn->rdb_txn));
+ int ret = check_reply(txn->rdb, reply, REDIS_REPLY_STATUS, txn->err);
+ if (ret != KNOT_EOK) {
+ freeReplyObject(reply);
+ return ret;
+ }
+
+ memset(txn, 0, sizeof(*txn));
+ freeReplyObject(reply);
+
+ return KNOT_EOK;
+}
+
+#else // ENABLE_REDIS
+
+struct redisContext *zone_redis_connect(conf_t *conf)
+{
+ return NULL;
+}
+
+void zone_redis_disconnect(struct redisContext *ctx)
+{
+ return;
+}
+
+int zone_redis_txn_begin(zone_redis_txn_t *txn, struct redisContext *rdb,
+ uint8_t instance, const knot_dname_t *zone_name,
+ bool incremental)
+{
+ return KNOT_ENOTSUP;
+}
+
+int zone_redis_write_rrset(zone_redis_txn_t *txn, const knot_rrset_t *rr)
+{
+ return KNOT_ENOTSUP;
+}
+
+int zone_redis_write_node(zone_redis_txn_t *txn, const zone_node_t *node)
+{
+ return KNOT_ENOTSUP;
+}
+
+int zone_redis_txn_commit(zone_redis_txn_t *txn)
+{
+ return KNOT_ENOTSUP;
+}
+
+int zone_redis_txn_abort(zone_redis_txn_t *txn)
+{
+ return KNOT_ENOTSUP;
+}
+
+#endif // ENABLE_REDIS
--- /dev/null
+/* Copyright (C) CZ.NIC, z.s.p.o. and contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * For more information, see <https://www.knot-dns.cz/>
+ */
+
+#pragma once
+
+#include "knot/conf/conf.h"
+#include "knot/zone/node.h"
+#include "redis/knot.h"
+
+#ifdef ENABLE_REDIS
+#include <hiredis/hiredis.h>
+#else // ENABLE_REDIS
+struct redisContext;
+#endif // ENABLE_REDIS
+
+typedef char zone_redis_err_t[128];
+
+typedef struct {
+ struct redisContext *rdb;
+ rdb_txn_t rdb_txn;
+ uint8_t instance;
+
+ const knot_dname_t *origin;
+ size_t origin_len;
+
+ bool incremental;
+ bool removals;
+
+ zone_redis_err_t err;
+} zone_redis_txn_t;
+
+/*!
+ * \brief Wrappers to rdb_connect and rdb_disconnect not needing #ifdef ENABLE_REDIS around.
+ */
+struct redisContext *zone_redis_connect(conf_t *conf);
+void zone_redis_disconnect(struct redisContext *ctx);
+
+/*!
+ * \brief Start a writing stransaction into Redis zone database.
+ *
+ * \param txn Transaction context structure to be filled;
+ * \param rdb Redis context (just pass zone_redis_connect()).
+ * \param instance Zone instance number (from configuration).
+ * \param zone_name Zone name.
+ * \param incremental Store incremental update (otherwise full zone rewrite).
+ *
+ * \return KNOT_E*
+ */
+int zone_redis_txn_begin(zone_redis_txn_t *txn, struct redisContext *rdb,
+ uint8_t instance, const knot_dname_t *zone_name,
+ bool incremental);
+
+/*!
+ * \brief Write single RRset to zone DB.
+ *
+ * \param txn Transaction to write into.
+ * \param rr RRset to write.
+ *
+ * \note In case of incremental transaction, txn->removals signals if the RRset should be added to removals or additions.
+ *
+ * \return KNOT_E*
+ */
+int zone_redis_write_rrset(zone_redis_txn_t *txn, const knot_rrset_t *rr);
+
+/*!
+ * \brief Calls zone_redis_write_rrset() for all RRsets in a node.
+ */
+int zone_redis_write_node(zone_redis_txn_t *txn, const zone_node_t *node);
+
+/*!
+ * \brief Commit a zone DB transaction.
+ */
+int zone_redis_txn_commit(zone_redis_txn_t *txn);
+
+/*!
+ * \brief Abort a zone DB transaction.
+ *
+ * \note You might want to ignore the return code.
+ */
+int zone_redis_txn_abort(zone_redis_txn_t *txn);
KNOT_ECPUCOMPAT,
KNOT_EMODINVAL,
KNOT_EEXTERNAL,
+ KNOT_ERDB,
KNOT_GENERAL_ERROR = -900,
{ KNOT_ECPUCOMPAT, "incompatible CPU architecture" },
{ KNOT_EMODINVAL, "invalid module" },
{ KNOT_EEXTERNAL, "external validation failed" },
+ { KNOT_ERDB, "zone database error" },
{ KNOT_GENERAL_ERROR, "unknown general error" },
'''Test master-slave-like replication using Redis database.'''
from dnstest.test import Test
+from dnstest.utils import *
t = Test(redis=True)
master.zones_wait(zones)
-master.ctl("zone-flush", wait=True)
-#slave.ctl("zone-reload")
-
+# Test zone stored by master and loaded by slave
serials = slave.zones_wait(zones)
t.xfr_diff(master, slave, zones)
+# Test incremental change stored by master and loaded by slave
for z in zones:
up = master.update(z)
up.add("suppnot1", 3600, "A", "1.2.3.4")
+ up.delete("mail", "A", "192.0.2.3")
+ up.send()
+
+serials2 = slave.zones_wait(zones, serials)
+t.xfr_diff(master, slave, zones) # AXFR diff
+t.xfr_diff(master, slave, zones, serials) # IXFR diff
+for z in zones:
+ resp = slave.dig("suppnot1." + z.name, "A")
+ resp.check(rcode="NOERROR", rdata="1.2.3.4")
+
+# Test yet another incremental change
+for z in zones:
+ up = master.update(z)
+ up.delete("suppnot1", "A", "1.2.3.4")
+ up.add("suppnot1", 1800, "A", "1.2.3.5")
up.send()
-t.sleep(2)
-master.ctl("zone-flush", wait=True)
-#slave.ctl("zone-reload")
+serials3 = slave.zones_wait(zones, serials2)
+t.xfr_diff(master, slave, zones)
+t.xfr_diff(master, slave, zones, serials)
+t.xfr_diff(master, slave, zones, serials2)
+for z in zones:
+ resp = slave.dig("suppnot1." + z.name, "A")
+ resp.check(rcode="NOERROR", nordata="1.2.3.4", rdata="1.2.3.5", ttl=1800)
+
+# Test no change
+slave.ctl("zone-reload", wait=True)
+uptodate_log = slave.log_search_count("database is up-to-date")
+if uptodate_log != len(zones):
+ set_err("UP-TO-DATE LOGGED %dx" % uptodate_log)
-slave.zones_wait(zones, serials)
+# Add to DB manually. Slave will diverge from master.
+for z in zones:
+ txn = t.redis.cli("knot.upd.begin", z.name, master.zones[z.name].redis_out)
+ r = t.redis.cli("knot.upd.remove", z.name, txn, "example.com. 3600 in soa dns1.example.com. hostmaster.example.com. %d 10800 3600 1209600 7200" % serials3[z.name])
+ r = t.redis.cli("knot.upd.add", z.name, txn, "example.com. 3600 in soa dns1.example.com. hostmaster.example.com. %d 10800 3600 1209600 7200" % (serials3[z.name] + 1))
+ r = t.redis.cli("knot.upd.add", z.name, txn, "txtadd 3600 A 1.2.3.4")
+ r = t.redis.cli("knot.upd.commit", z.name, txn)
+
+ r = t.redis.cli("knot.upd.load", z.name, master.zones[z.name].redis_out, str(serials3[z.name]))
+ if not "txtadd" in r:
+ set_err("NO TXTADD IN UPD")
+
+serials4 = slave.zones_wait(zones, serials3)
+for z in zones:
+ resp = slave.dig("txtadd." + z.name, "A")
+ resp.check(rcode="NOERROR", rdata="1.2.3.4")
+
+# Update master with double SOA increment, it shall overwrite with greater serial and different contents.
+for z in zones:
+ up = master.update(z)
+ up.add(z.name, 3600, "SOA", "dns1.example.com. hostmaster.example.com. %d 10800 3600 1209600 7200" % (serials3[z.name] + 2))
+ up.delete("suppnot1", "A", "1.2.3.5")
+ up.add("suppnot1", 900, "A", "1.2.3.5")
+ up.send()
+
+serials5 = slave.zones_wait(zones, serials4)
+for z in zones:
+ resp = slave.dig("txtadd." + z.name, "A")
+ resp.check(rcode="NXDOMAIN", nordata="1.2.3.4")
+ resp = slave.dig("suppnot1." + z.name, "A")
+ resp.check(rcode="NOERROR", nordata="1.2.3.4", rdata="1.2.3.5", ttl=900)
t.xfr_diff(master, slave, zones)
# SOA serial logic rotation