]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
redis: command knot.zone.info
authorJan Hák <jan.hak@nic.cz>
Mon, 22 Sep 2025 12:00:24 +0000 (14:00 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Wed, 5 Nov 2025 14:31:33 +0000 (15:31 +0100)
doc/operation.rst
src/redis/internal.h
src/redis/knot.c
tests-redis/test.py

index 8ee919d743a1da20a1a1d957e3388339f1c50705..47dd1c4e83212d5941aaf1cf453b0ece74787907 100644 (file)
@@ -490,6 +490,8 @@ The ``KNOT.ZONE.*`` commands are used for manipulating entire zones:
 - ``KNOT.ZONE.PURGE <zone>`` — Purges all data for the specified zone.
 - ``KNOT.ZONE.LIST [--instances]`` — Lists stored zones, optionally including
   available instances.
+- ``KNOT.ZONE.INFO [zone] [instance]`` — Print info about zones and its updates,
+  optionally filtered by zone and instance number.
 
 Example of a zone initialization::
 
@@ -581,6 +583,12 @@ Example of a zone update::
       2) 1) (empty array)
          2) 1) "test.example.com. 600 TXT \"Knot DNS\""
 
+   $ redis-cli KNOT.ZONE.INFO
+   1) 1) "example.com."
+      2) 1) 1) "instance: 1"
+            2) "serial: 2"
+            3) 1) "update: 1 -> 2"
+
 .. WARNING::
    Do not modify the zone data using native commands such as SET or ZADD,
    as this may cause data corruption!
index 215bbdfe1326b6f8c4463748693c9fd5532b96bd..8d94ef1c2d42fc7bd8bf88ff648fb3d89550b106 100644 (file)
@@ -1296,14 +1296,15 @@ typedef struct {
        RedisModuleCtx *ctx;
        size_t count;
        int ret;
-       bool txt;
-       bool instances;
-} scan_ctx;
+       uint8_t instance; // Used by zone_info
+       bool txt;         // Used by zone_list
+       bool instances;   // Used by zone_list
+} scan_ctx_t;
 
 static void zone_list_cb(RedisModuleKey *key, RedisModuleString *zone_name,
                          RedisModuleString *mask, void *privdata)
 {
-       scan_ctx *sctx = privdata;
+       scan_ctx_t *sctx = privdata;
        if (sctx->txt) {
                size_t len;
                const char *dname = RedisModule_StringPtrLen(zone_name, &len);
@@ -1359,11 +1360,12 @@ static void zone_list(RedisModuleCtx *ctx, bool instances, bool txt)
 
        RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
 
-       scan_ctx sctx = {
+       scan_ctx_t sctx = {
                .ctx = ctx,
                .txt = txt,
                .instances = instances
        };
+
        RedisModuleScanCursor *cursor = RedisModule_ScanCursorCreate();
        while (RedisModule_ScanKey(zones_index, cursor, zone_list_cb, &sctx) && sctx.ret == KNOT_EOK);
        RedisModule_ReplySetArrayLength(ctx, sctx.count);
@@ -1371,6 +1373,169 @@ static void zone_list(RedisModuleCtx *ctx, bool instances, bool txt)
        RedisModule_ScanCursorDestroy(cursor);
 }
 
+exception_t zone_info_serial(RedisModuleCtx *ctx, size_t *counter, const arg_dname_t *origin,
+                             const rdb_txn_t *txn, const uint32_t serial_end, const uint32_t serial)
+{
+       index_k upd_index_key = get_commited_upd_index(ctx, origin, txn, serial, REDISMODULE_READ);
+       if (upd_index_key == NULL) {
+               return_ok;
+       }
+
+       uint32_t serial_next = 0;
+       exception_t e = index_soa_serial(ctx, upd_index_key, true, &serial_next);
+       if (e.ret != KNOT_EOK) {
+               RedisModule_CloseKey(upd_index_key);
+               raise(e);
+       }
+
+       if (serial_next != serial_end) {
+               e = zone_info_serial(ctx, counter, origin, txn, serial_end, serial_next);
+               if (e.ret != KNOT_EOK) {
+                       RedisModule_CloseKey(upd_index_key);
+                       raise(e);
+               }
+       }
+
+       char buf[64];
+       (void)snprintf(buf, sizeof(buf), "update: %u -> %u", serial_next, serial);
+       RedisModule_ReplyWithCString(ctx, buf);
+       *counter += 1;
+
+       RedisModule_CloseKey(upd_index_key);
+
+       return_ok;
+}
+
+static void zone_info_serials(RedisModuleCtx *ctx, arg_dname_t *origin, rdb_txn_t *txn)
+{
+       if (set_active_transaction(ctx, origin, txn) != KNOT_EOK) {
+               RedisModule_ReplyWithError(ctx, RDB_EZONE);
+               return;
+       }
+
+       RedisModuleString *soa_rrset_keyname = rrset_keyname_construct(ctx, origin, txn, origin, KNOT_RRTYPE_SOA);
+       rrset_k soa_rrset_key = RedisModule_OpenKey(ctx, soa_rrset_keyname, REDISMODULE_READ);
+       RedisModule_FreeString(ctx, soa_rrset_keyname);
+       rrset_v *rrset = RedisModule_ModuleTypeGetValue(soa_rrset_key);
+       if (rrset == NULL) {
+               RedisModule_CloseKey(soa_rrset_key);
+               RedisModule_ReplyWithError(ctx, RDB_ENOSOA);
+               return;
+       }
+       uint32_t serial_it = knot_soa_serial(rrset->rrs.rdata);
+       RedisModule_CloseKey(soa_rrset_key);
+
+       RedisModule_ReplyWithArray(ctx, 3);
+       char buf[64];
+       (void)snprintf(buf, sizeof(buf), "instance: %u", txn->instance);
+       RedisModule_ReplyWithCString(ctx, buf);
+       (void)snprintf(buf, sizeof(buf), "serial: %u", serial_it);
+       RedisModule_ReplyWithCString(ctx, buf);
+
+       size_t upd_count = 0;
+       RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+       exception_t e = zone_info_serial(ctx, &upd_count, origin, txn, serial_it, serial_it);
+       if (e.ret != KNOT_EOK && e.what != NULL) {
+               RedisModule_ReplyWithError(ctx, e.what);
+               upd_count++;
+       }
+       RedisModule_ReplySetArrayLength(ctx, upd_count);
+}
+
+static void zone_info_cb(RedisModuleKey *key, RedisModuleString *zone_name,
+                         RedisModuleString *mask, void *privdata)
+{
+       scan_ctx_t *sctx = privdata;
+
+       char buf[KNOT_DNAME_TXT_MAXLEN];
+
+       size_t len;
+       const char *dname = RedisModule_StringPtrLen(zone_name, &len);
+       arg_dname_t origin = { .data = (uint8_t *)dname, .len = len };
+       RedisModule_Assert(knot_dname_to_str(buf, (knot_dname_t *)dname, sizeof(buf)) != NULL);
+
+       const uint8_t *mask_p = (const uint8_t *)RedisModule_StringPtrLen(mask, &len);
+       RedisModule_Assert(len == sizeof(uint8_t));
+
+       RedisModule_ReplyWithArray(sctx->ctx, 2);
+       RedisModule_ReplyWithCString(sctx->ctx, buf);
+
+       RedisModule_ReplyWithArray(sctx->ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+       size_t inst_count = 0;
+       if (sctx->instance != 0) {
+               if ((*mask_p) & (1 << (sctx->instance - 1))) {
+                       rdb_txn_t txn = {
+                               .instance = sctx->instance,
+                               .id = TXN_ID_ACTIVE
+                       };
+                       zone_info_serials(sctx->ctx, &origin, &txn);
+                       ++inst_count;
+               }
+       } else {
+               for (unsigned inst = 1; inst <= INSTANCE_MAX; ++inst) {
+                       if ((*mask_p) & (1 << (inst - 1))) {
+                               rdb_txn_t txn = {
+                                       .instance = inst,
+                                       .id = TXN_ID_ACTIVE
+                               };
+                               zone_info_serials(sctx->ctx, &origin, &txn);
+                               ++inst_count;
+                       }
+               }
+       }
+       RedisModule_ReplySetArrayLength(sctx->ctx, inst_count);
+
+       ++(sctx->count);
+}
+
+static void zone_info(RedisModuleCtx *ctx, arg_dname_t *origin, rdb_txn_t *txn)
+{
+       scan_ctx_t sctx = {
+               .ctx = ctx,
+               .instance = (txn != NULL) ? txn->instance : 0
+       };
+
+       RedisModuleKey *zones_index = get_zones_index(ctx, REDISMODULE_READ);
+       if (zones_index == NULL) {
+               RedisModule_ReplyWithError(ctx, RDB_EALLOC);
+               return;
+       }
+
+       if (origin != NULL) {
+               RedisModuleString *origin_str = RedisModule_CreateString(ctx, (const char *)origin->data, origin->len);
+               if (origin_str == NULL) {
+                       RedisModule_CloseKey(zones_index);
+                       RedisModule_ReplyWithError(ctx, RDB_EALLOC);
+                       return;
+               }
+
+               RedisModuleString *value;
+               if (RedisModule_HashGet(zones_index, REDISMODULE_HASH_NONE, origin_str, &value, NULL) != REDISMODULE_OK) {
+                       RedisModule_FreeString(ctx, origin_str);
+                       RedisModule_CloseKey(zones_index);
+                       RedisModule_ReplyWithError(ctx, RDB_ECORRUPTED);
+                       return;
+               }
+               zone_info_cb(zones_index, origin_str, value, &sctx);
+               RedisModule_FreeString(ctx, value);
+               RedisModule_FreeString(ctx, origin_str);
+       } else {
+               RedisModuleScanCursor *cursor = RedisModule_ScanCursorCreate();
+               if (cursor == NULL) {
+                       RedisModule_CloseKey(zones_index);
+                       RedisModule_ReplyWithError(ctx, RDB_EALLOC);
+                       return;
+               }
+
+               RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+               while (RedisModule_ScanKey(zones_index, cursor, zone_info_cb, &sctx) && sctx.ret == KNOT_EOK);
+               RedisModule_ReplySetArrayLength(ctx, sctx.count);
+
+               RedisModule_ScanCursorDestroy(cursor);
+       }
+       RedisModule_CloseKey(zones_index);
+}
+
 static exception_t zone_meta_active_exchange(RedisModuleCtx *ctx, zone_meta_k key,
                                              const arg_dname_t *origin, rdb_txn_t *txn)
 {
index 5074b9ab22f1019efd798cae9d43505b6ae3815b..f6235bbf987de46930116670c890ebbab3ebba09 100644 (file)
@@ -70,6 +70,12 @@ static RedisModuleCommandArg zone_list_txt_info_args[] = {
        { 0 }
 };
 
+static RedisModuleCommandArg zone_info_txt_info_args[] = {
+       {"zone",     REDISMODULE_ARG_TYPE_STRING,     -1, NULL, NULL, NULL, REDISMODULE_CMD_ARG_OPTIONAL},
+       {"instance", REDISMODULE_ARG_TYPE_INTEGER,    -1, NULL, NULL, NULL, REDISMODULE_CMD_ARG_OPTIONAL},
+       { 0 }
+};
+
 static const RedisModuleCommandInfo zone_begin_txt_info = {
        .version = REDISMODULE_COMMAND_INFO_VERSION,
        .summary = "Create a zone full transaction",
@@ -133,6 +139,15 @@ static const RedisModuleCommandInfo zone_list_txt_info = {
        .args = zone_list_txt_info_args,
 };
 
+static const RedisModuleCommandInfo zone_info_txt_info = {
+       .version = REDISMODULE_COMMAND_INFO_VERSION,
+       .summary = "List zones stored in the database showing serials and updates",
+       .complexity = "O(z), where z is the number of zones",
+       .since = "7.0.0",
+       .arity = -1,
+       .args = zone_info_txt_info_args,
+};
+
 static const RedisModuleCommandInfo upd_begin_txt_info = {
        .version = REDISMODULE_COMMAND_INFO_VERSION,
        .summary = "Create an zone update transaction",
@@ -452,6 +467,22 @@ static int zone_list_bin(RedisModuleCtx *ctx, RedisModuleString **argv, int argc
        return REDISMODULE_OK;
 }
 
+static int zone_info_txt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+       arg_dname_t origin;
+       if (argc >= 2) {
+               ARG_DNAME_TXT(argv[1], origin, NULL, "zone origin");
+       }
+
+       rdb_txn_t txn;
+       if (argc >= 3) {
+               ARG_INST_TXT(argv[2], txn);
+       }
+
+       zone_info(ctx, (argc >= 2) ? &origin : NULL, (argc >= 3) ? &txn : NULL);
+       return REDISMODULE_OK;
+}
+
 static int upd_begin_txt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
 {
        arg_dname_t origin;
@@ -848,6 +879,7 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
            register_command_txt("KNOT.ZONE.LOAD",     zone_load_txt,     "readonly")   ||
            register_command_txt("KNOT.ZONE.PURGE",    zone_purge_txt,    "write")      ||
            register_command_txt("KNOT.ZONE.LIST",     zone_list_txt,     "readonly")   ||
+           register_command_txt("KNOT.ZONE.INFO",     zone_info_txt,     "readonly")   ||
            register_command_txt("KNOT.UPD.BEGIN",     upd_begin_txt,     "write fast") ||
            register_command_txt("KNOT.UPD.ADD",       upd_add_txt,       "write fast") ||
            register_command_txt("KNOT.UPD.REMOVE",    upd_remove_txt,    "write fast") ||
index 8f4542e065d4cf2fcb395717f3f0bfd2a29769aa..8a6cac71595b99f89bf13fbc413edb3180caf8fe 100644 (file)
@@ -283,6 +283,18 @@ def test_zone_list():
     resp = env.cmd('KNOT.ZONE.LIST', txn_get_instance(txn))
     env.assertEqual(len(resp), 2, message="Failed to purge zone")
 
+    # zone info
+    INFO = [
+        [b'example.com.', [[b'instance: 1', b'serial: 1', []]]],
+        [b'example.net.', [[b'instance: 1', b'serial: 1', []]]]
+    ]
+
+    resp = env.cmd('KNOT.ZONE.INFO')
+    env.assertEqual(resp, INFO, message="Failed to info zones")
+
+    resp = env.cmd('KNOT.ZONE.INFO', 'example.com', '1')
+    env.assertEqual(resp, INFO[0], message="Failed to info zones")
+
 def test_upd_begin():
     env = Env(moduleArgs=['max-event-age', '60', 'default-ttl', '3600'])