]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
knotc: implemented zone-serial-set
authorLibor Peltan <libor.peltan@nic.cz>
Thu, 2 Oct 2025 13:08:30 +0000 (15:08 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Wed, 15 Oct 2025 10:24:33 +0000 (12:24 +0200)
doc/man_knotc.rst
src/knot/ctl/commands.c
src/knot/ctl/commands.h
src/knot/updates/zone-update.c
src/knot/updates/zone-update.h
src/utils/knotc/commands.c
src/utils/knotc/commands.h
tests-extra/tests/ctl/concurrent/test.py

index 595c2f84a1befed8d7051a8900d3cdcaa9248bc8..33368f36a5e6f02a244a5f1781d3f143d7651833 100644 (file)
@@ -244,6 +244,15 @@ Actions
   Show zone statistics counter(s). To print also counters with value 0, use
   force option.
 
+**zone-serial-set** *zone* [[**+**]\ *serial*]
+  Set the zone SOA serial to the specified value. If the argument is prefixed with
+  **+**, the serial is incremented by that value (the special constant **MAX**
+  can be used to increment by the maximum allowed value 2147483647).
+  If the zone transaction is open, both reading and writing of the SOA serial
+  occur within that transaction. Otherwise, a short-lived transaction
+  is created and committed solely for setting the new SOA serial.
+  If no argument is specified, the current zone SOA serial is returned.
+
 **conf-init**
   Initialize the configuration database. If the database doesn't exist yet,
   execute this command as an intended user to ensure the server is permitted
index 94472099fb07f7754084e8ad76766a2e0c7509ab..433ead8fbec42f44d457673163687c8c0734eace 100644 (file)
@@ -1004,25 +1004,96 @@ static int zone_txn_commit(zone_t *zone, ctl_args_t *args)
        return ret;
 }
 
-static int zone_txn_abort(zone_t *zone, _unused_ ctl_args_t *args)
+static int zone_txn_abort_l(zone_t *zone, _unused_ ctl_args_t *args)
 {
-       pthread_mutex_lock(&zone->cu_lock);
        if (zone->control_update == NULL) {
                args->suppress = true;
-               pthread_mutex_unlock(&zone->cu_lock);
                return KNOT_TXN_ENOTEXISTS;
        }
 
        if (zone->control_update->flags & UPDATE_WFEV) {
                knot_sem_post(&zone->control_update->external);
-               pthread_mutex_unlock(&zone->cu_lock);
                return KNOT_EOK;
        }
 
        zone_control_clear(zone);
+       return KNOT_EOK;
+}
 
+static int zone_txn_abort(zone_t *zone, ctl_args_t *args)
+{
+       pthread_mutex_lock(&zone->cu_lock);
+       int ret = zone_txn_abort_l(zone, args);
        pthread_mutex_unlock(&zone->cu_lock);
-       return KNOT_EOK;
+       return ret;
+}
+
+static int zone_serial_set(zone_t *zone, ctl_args_t *args)
+{
+       int ret = KNOT_EOK;
+       const char *serial_set_str = args->data[KNOT_CTL_IDX_DATA];
+       const char *serial_inc_str = args->data[KNOT_CTL_IDX_TYPE];
+       bool forced = ctl_has_flag(args->data[KNOT_CTL_IDX_FLAGS], CTL_FLAG_FORCE);
+       bool serial_set_do = (serial_set_str != NULL);
+       bool serial_inc_do = (serial_inc_str != NULL && serial_inc_str[0] == '+');
+
+       uint32_t serial_set = 0;
+       if (serial_set_do) {
+               ret = str_to_u32(serial_set_str, &serial_set);
+               if (ret != KNOT_EOK) {
+                       return ret;
+               }
+       }
+
+       rcu_read_lock();
+       const zone_contents_t *c = zone->contents;
+       if (c == NULL) {
+               rcu_read_unlock();
+               return KNOT_EEMPTYZONE;
+       }
+       uint32_t return_serial = zone_contents_serial(c);
+       rcu_read_unlock();
+
+       pthread_mutex_lock(&zone->cu_lock);
+       bool cu_exists = (zone->control_update != NULL);
+
+       if (serial_set_do && !cu_exists) {
+               ret = zone_txn_begin_l(zone, args);
+       }
+
+       if (zone->control_update != NULL && ret == KNOT_EOK &&
+           !node_rrtype_exists(zone->control_update->new_cont->apex, KNOT_RRTYPE_SOA)) {
+               ret = KNOT_EEMPTYZONE;
+       }
+
+       if (zone->control_update != NULL && ret == KNOT_EOK) {
+               return_serial = zone_update_current_serial(zone->control_update);
+               if (serial_inc_do) {
+                       serial_set += return_serial;
+               }
+               if (serial_set_do) {
+                       ret = zone_update_set_soa(zone->control_update, serial_set, !forced);
+               }
+       }
+
+       if (serial_set_do && !cu_exists) {
+               if (ret == KNOT_EOK) {
+                       ret = zone_txn_commit_l(zone, args);
+               }
+               if (ret != KNOT_EOK) {
+                       zone_txn_abort_l(zone, args);
+               }
+       }
+       pthread_mutex_unlock(&zone->cu_lock);
+
+       if (!serial_set_do && ret == KNOT_EOK) {
+               send_ctx_t *ctx = &ctl_globals[args->thread_idx].send_ctx;
+               ctx->data[KNOT_CTL_IDX_DATA] = ctx->ttl;
+               (void)snprintf(ctx->ttl, sizeof(ctx->ttl), "%u", return_serial);
+               ret = knot_ctl_send(args->ctl, KNOT_CTL_TYPE_DATA, &ctx->data);
+       }
+
+       return ret;
 }
 
 static int init_send_ctx(send_ctx_t *ctx, const knot_dname_t *zone_name,
@@ -1913,6 +1984,8 @@ static int ctl_zone(ctl_args_t *args, ctl_cmd_t cmd)
                }
        case CTL_ZONE_STATS:
                return zones_apply(args, zone_stats);
+       case CTL_ZONE_SERIAL_SET:
+               return zones_apply(args, zone_serial_set);
        default:
                assert(0);
                return KNOT_EINVAL;
@@ -2386,6 +2459,7 @@ static const desc_t cmd_table[] = {
        [CTL_ZONE_UNSET]      = { "zone-unset",         ctl_zone,         CTL_LOCK_SRV_R },
        [CTL_ZONE_PURGE]      = { "zone-purge",         ctl_zone,         CTL_LOCK_SRV_W },
        [CTL_ZONE_STATS]      = { "zone-stats",         ctl_zone,         CTL_LOCK_SRV_R },
+       [CTL_ZONE_SERIAL_SET] = { "zone-serial-set",    ctl_zone,         CTL_LOCK_SRV_R },
 
        [CTL_CONF_LIST]       = { "conf-list",          ctl_conf_list,    CTL_LOCK_SRV_R }, // Can either read live conf or conf txn. The latter would deserve CTL_LOCK_SRV_W, but when conf txn exists, all cmds are done by single thread anyway.
        [CTL_CONF_READ]       = { "conf-read",          ctl_conf_read,    CTL_LOCK_SRV_R },
index ea2b32dff7482c0be837d0e629ff38500b4c4622..25b893514abbbeed27766779549096d92f8003b8 100644 (file)
@@ -108,6 +108,7 @@ typedef enum {
        CTL_ZONE_UNSET,
        CTL_ZONE_PURGE,
        CTL_ZONE_STATS,
+       CTL_ZONE_SERIAL_SET,
 
        CTL_CONF_LIST,
        CTL_CONF_READ,
index 737ca5e2cd5b48904475bb96b533d216adf25e76..74572ba8f190c99250056f8009d3af2f918e7bb8 100644 (file)
@@ -630,9 +630,9 @@ int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t
        return zone_update_apply_changeset(update, &reverse);
 }
 
-int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
+int zone_update_set_soa(zone_update_t *update, uint32_t new_serial, bool semcheck)
 {
-       if (update == NULL || conf == NULL) {
+       if (update == NULL) {
                return KNOT_EINVAL;
        }
 
@@ -642,26 +642,38 @@ int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
                return KNOT_ENOMEM;
        }
 
-       int ret = zone_update_remove(update, soa_cpy);
-       if (ret != KNOT_EOK) {
+       uint32_t old_serial = knot_soa_serial(soa_cpy->rrs.rdata);
+       if (semcheck && serial_compare(old_serial, new_serial) != SERIAL_LOWER) {
                knot_rrset_free(soa_cpy, NULL);
-               return ret;
+               return KNOT_ESOAINVAL;
        }
 
-       uint32_t old_serial = knot_soa_serial(soa_cpy->rrs.rdata);
+       int ret = zone_update_remove(update, soa_cpy);
+       if (ret == KNOT_EOK) {
+               knot_soa_serial_set(soa_cpy->rrs.rdata, new_serial);
+               ret = zone_update_add(update, soa_cpy);
+       }
+       knot_rrset_free(soa_cpy, NULL);
+
+       return ret;
+}
+
+int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
+{
+       if (update == NULL || conf == NULL) {
+               return KNOT_EINVAL;
+       }
+
+       uint32_t old_serial = zone_update_current_serial(update);
        uint32_t new_serial = serial_next(old_serial, conf, update->zone->name,
                                          SERIAL_POLICY_AUTO, 1);
-       if (serial_compare(old_serial, new_serial) != SERIAL_LOWER) {
+
+       int ret = zone_update_set_soa(update, new_serial, true);
+       if (ret == KNOT_ESOAINVAL) {
                log_zone_warning(update->zone->name, "updated SOA serial is lower "
                                 "than current, serial %u -> %u",
                                 old_serial, new_serial);
-               ret = KNOT_ESOAINVAL;
-       } else {
-               knot_soa_serial_set(soa_cpy->rrs.rdata, new_serial);
-
-               ret = zone_update_add(update, soa_cpy);
        }
-       knot_rrset_free(soa_cpy, NULL);
 
        return ret;
 }
index 7e0506c6bdcb166d6647f446debbb97da623422b..93b05196daaa3bba213154c6a7e802187a559469 100644 (file)
@@ -242,12 +242,25 @@ int zone_update_apply_changeset(zone_update_t *update, const changeset_t *change
  */
 int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t *changes);
 
+/*!
+ * \brief Set SOA serial in the update.
+ *
+ * \param update        Update to be modified.
+ * \param new_serial    SOA serial to be set.
+ * \param semcheck      Enable serial decrement check.
+ *
+ * \retval KNOT_ESOAINVAL if updated serial is lower than current and semcheck enabled.
+ * \return KNOT_E*
+ */
+int zone_update_set_soa(zone_update_t *update, uint32_t new_serial, bool semcheck);
+
 /*!
  * \brief Increment SOA serial (according to configured policy) in the update.
  *
  * \param update  Update to be modified.
  * \param conf    Configuration.
  *
+ * \retval KNOT_ESOAINVAL if updated serial is lower than current.
  * \return KNOT_E*
  */
 int zone_update_increment_soa(zone_update_t *update, conf_t *conf);
index 73e01063e2b69c25b8fa7269a0d3b94a12413368..3e78ba307ce618c8947406e55527c4a83a92be57 100644 (file)
@@ -325,6 +325,15 @@ static void format_data(cmd_args_t *args, knot_ctl_type_t data_type,
                       (value != NULL ? value      : ""));
                *empty = false;
                break;
+       case CTL_ZONE_SERIAL_SET:
+               if (error != NULL) {
+                       printf("error: (%s)", error);
+                       *empty = false;
+               } else if (value != NULL) {
+                       printf("%s", value);
+                       *empty = false;
+               }
+               break;
        default:
                assert(0);
        }
@@ -366,7 +375,8 @@ static void format_block(ctl_cmd_t cmd, bool failed, bool empty)
        case CTL_ZONE_SET:
        case CTL_ZONE_UNSET:
        case CTL_ZONE_PURGE:
-               printf("%s\n", failed ? "" : "OK");
+       case CTL_ZONE_SERIAL_SET:
+               printf("%s\n", (failed || !empty) ? "" : "OK");
                break;
        case CTL_STATUS:
        case CTL_ZONE_STATUS:
@@ -593,6 +603,35 @@ static int cmd_zone_key_roll_ctl(cmd_args_t *args)
        return ctl_receive(args);
 }
 
+static int cmd_zone_serial_ctl(cmd_args_t *args)
+{
+       int ret = check_args(args, 1, 2);
+       if (ret != KNOT_EOK) {
+               return ret;
+       }
+
+       knot_ctl_data_t data = {
+               [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+               [KNOT_CTL_IDX_FLAGS] = *args->flags ? args->flags : NULL,
+               [KNOT_CTL_IDX_ZONE] = args->argv[0],
+       };
+
+       if (args->argc > 1) {
+               bool incr = args->argv[1][0] == '+';
+               const char *value = incr ? args->argv[1] + 1 : args->argv[1];
+               if (incr && strcmp("MAX", value) == 0) {
+                       value = "2147483647";
+               }
+               data[KNOT_CTL_IDX_TYPE] = incr ? "+" : "=";
+               data[KNOT_CTL_IDX_DATA] = value;
+       }
+
+       CTL_SEND_DATA
+       CTL_SEND_BLOCK
+
+       return ctl_receive(args);
+}
+
 const filter_desc_t conf_import_filters[] = {
        { "+nopurge" },
        { NULL },
@@ -1229,6 +1268,7 @@ const cmd_desc_t cmd_table[] = {
        { CMD_ZONE_UNSET,      cmd_zone_node_ctl,   CTL_ZONE_UNSET,      CMD_FREQ_ZONE },
        { CMD_ZONE_PURGE,      cmd_zone_ctl,        CTL_ZONE_PURGE,      CMD_FREQ_ZONE | CMD_FOPT_ZONE | CMD_FOPT_FILTER },
        { CMD_ZONE_STATS,      cmd_stats_ctl,       CTL_ZONE_STATS,      CMD_FREQ_ZONE },
+       { CMD_ZONE_SERIAL_SET, cmd_zone_serial_ctl, CTL_ZONE_SERIAL_SET, CMD_FREQ_ZONE },
 
        { CMD_CONF_INIT,       cmd_conf_init,     CTL_NONE,            CMD_FWRITE },
        { CMD_CONF_CHECK,      cmd_conf_check,    CTL_NONE,            CMD_FREAD  | CMD_FREQ_MOD | CMD_FLOG_MORE },
@@ -1283,6 +1323,7 @@ static const cmd_help_t cmd_help_table[] = {
        { CMD_ZONE_UNSET,      "<zone>  <owner> [<type> [<rdata>]]",         "Remove zone data within the transaction." },
        { CMD_ZONE_PURGE,      "<zone>... [<filter>...]",                    "Purge zone data, zone file, journal, timers, and KASP data. (#)" },
        { CMD_ZONE_STATS,      "<zone> [<module>[.<counter>]]",              "Show zone statistics counter(s)."},
+       { CMD_ZONE_SERIAL_SET, "<zone> [[+]<number>]",                       "Get/set/increment SOA serial of given zone. (#)" },
        { "",                  "",                                           "" },
        { CMD_CONF_INIT,       "",                                           "Initialize the confdb. (*)" },
        { CMD_CONF_CHECK,      "",                                           "Check the server configuration. (*)" },
index 87ad5fd62d1397215fbecb32dde4fc6f5a55e3e6..98667d348537d9a3846aaa627aeb181fe2c63441 100644 (file)
@@ -44,6 +44,7 @@
 #define CMD_ZONE_UNSET         "zone-unset"
 #define CMD_ZONE_PURGE         "zone-purge"
 #define CMD_ZONE_STATS         "zone-stats"
+#define CMD_ZONE_SERIAL_SET    "zone-serial-set"
 
 #define CMD_CONF_INIT          "conf-init"
 #define CMD_CONF_CHECK         "conf-check"
index 5490ada15689a130820365a2e0bcc296fd26ef62..8193a82ae432bfeb60413ebf4f90931abe60889a 100644 (file)
@@ -23,9 +23,11 @@ def random_ctl(server, zone_name):
                          "zone-sign", "zone-keys-load", "zone-ksk-submitted", "zone-freeze",
                          "zone-thaw", "zone-xfr-freeze", "zone-xfr-thaw", "zone-read", "zone-get",
                          "zone-stats", "conf-init", "conf-list", "conf-read", "conf-diff",
-                         "conf-get"])
+                         "conf-get", "zone-serial-set"])
     if cmd[0:5] == "zone-" and random.choice([False, True, True]):
         cmd += " " + zone_name
+    if cmd == "zone-serial-set" and random.choice([False, True, True]):
+        cmd += " " + random.choice(["+", "="]) + str(random.randint(0, 4000000000))
     if random.choice([False, True]):
         cmd = "-b " + cmd
     try:
@@ -106,7 +108,8 @@ def bck_purge_rest(server, zone_name):
 
 def ctl_update(server, zone_name):
     ctl_txn_generic(server, "-b zone-begin " + zone_name,
-                    "zone-set " + zone_name + " abc 3600 A 1.2.3." + str(random.randint(1, 254)),
+                    random.choice(["zone-set " + zone_name + " abc 3600 A 1.2.3." + str(random.randint(1, 254)),
+                                   "zone-serial-set " + zone_name + " " + random.choice(["+", "="]) + str(random.randint(0, 4000000000))]),
                     "zone-commit " + zone_name, "zone-abort " + zone_name, True)
 
 def conf_update(server, zone_name):