From: Jan Hák Date: Mon, 22 Sep 2025 12:00:24 +0000 (+0200) Subject: redis: command knot.zone.info X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=356e9c4987bdb24e1758df233a4ae6c367748f9a;p=thirdparty%2Fknot-dns.git redis: command knot.zone.info --- diff --git a/doc/operation.rst b/doc/operation.rst index 8ee919d743..47dd1c4e83 100644 --- a/doc/operation.rst +++ b/doc/operation.rst @@ -490,6 +490,8 @@ The ``KNOT.ZONE.*`` commands are used for manipulating entire zones: - ``KNOT.ZONE.PURGE `` — 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! diff --git a/src/redis/internal.h b/src/redis/internal.h index 215bbdfe13..8d94ef1c2d 100644 --- a/src/redis/internal.h +++ b/src/redis/internal.h @@ -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) { diff --git a/src/redis/knot.c b/src/redis/knot.c index 5074b9ab22..f6235bbf98 100644 --- a/src/redis/knot.c +++ b/src/redis/knot.c @@ -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") || diff --git a/tests-redis/test.py b/tests-redis/test.py index 8f4542e065..8a6cac7159 100644 --- a/tests-redis/test.py +++ b/tests-redis/test.py @@ -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'])