summaryrefslogtreecommitdiffstats
path: root/src/utils/knotc
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/utils/knotc/commands.c1340
-rw-r--r--src/utils/knotc/commands.h74
-rw-r--r--src/utils/knotc/interactive.c450
-rw-r--r--src/utils/knotc/interactive.h26
-rw-r--r--src/utils/knotc/main.c172
-rw-r--r--src/utils/knotc/process.c291
-rw-r--r--src/utils/knotc/process.h78
7 files changed, 2431 insertions, 0 deletions
diff --git a/src/utils/knotc/commands.c b/src/utils/knotc/commands.c
new file mode 100644
index 0000000..abfb12b
--- /dev/null
+++ b/src/utils/knotc/commands.c
@@ -0,0 +1,1340 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libknot/libknot.h"
+#include "knot/common/log.h"
+#include "knot/ctl/commands.h"
+#include "knot/conf/conf.h"
+#include "knot/conf/confdb.h"
+#include "knot/conf/tools.h"
+#include "knot/zone/zonefile.h"
+#include "knot/zone/zone-load.h"
+#include "contrib/color.h"
+#include "contrib/macros.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "contrib/openbsd/strlcat.h"
+#include "utils/knotc/commands.h"
+
+#define CMD_EXIT "exit"
+
+#define CMD_STATUS "status"
+#define CMD_STOP "stop"
+#define CMD_RELOAD "reload"
+#define CMD_STATS "stats"
+
+#define CMD_ZONE_CHECK "zone-check"
+#define CMD_ZONE_STATUS "zone-status"
+#define CMD_ZONE_RELOAD "zone-reload"
+#define CMD_ZONE_REFRESH "zone-refresh"
+#define CMD_ZONE_RETRANSFER "zone-retransfer"
+#define CMD_ZONE_NOTIFY "zone-notify"
+#define CMD_ZONE_FLUSH "zone-flush"
+#define CMD_ZONE_BACKUP "zone-backup"
+#define CMD_ZONE_RESTORE "zone-restore"
+#define CMD_ZONE_SIGN "zone-sign"
+#define CMD_ZONE_KEYS_LOAD "zone-keys-load"
+#define CMD_ZONE_KEY_ROLL "zone-key-rollover"
+#define CMD_ZONE_KSK_SBM "zone-ksk-submitted"
+#define CMD_ZONE_FREEZE "zone-freeze"
+#define CMD_ZONE_THAW "zone-thaw"
+#define CMD_ZONE_XFR_FREEZE "zone-xfr-freeze"
+#define CMD_ZONE_XFR_THAW "zone-xfr-thaw"
+
+#define CMD_ZONE_READ "zone-read"
+#define CMD_ZONE_BEGIN "zone-begin"
+#define CMD_ZONE_COMMIT "zone-commit"
+#define CMD_ZONE_ABORT "zone-abort"
+#define CMD_ZONE_DIFF "zone-diff"
+#define CMD_ZONE_GET "zone-get"
+#define CMD_ZONE_SET "zone-set"
+#define CMD_ZONE_UNSET "zone-unset"
+#define CMD_ZONE_PURGE "zone-purge"
+#define CMD_ZONE_STATS "zone-stats"
+
+#define CMD_CONF_INIT "conf-init"
+#define CMD_CONF_CHECK "conf-check"
+#define CMD_CONF_IMPORT "conf-import"
+#define CMD_CONF_EXPORT "conf-export"
+#define CMD_CONF_LIST "conf-list"
+#define CMD_CONF_READ "conf-read"
+#define CMD_CONF_BEGIN "conf-begin"
+#define CMD_CONF_COMMIT "conf-commit"
+#define CMD_CONF_ABORT "conf-abort"
+#define CMD_CONF_DIFF "conf-diff"
+#define CMD_CONF_GET "conf-get"
+#define CMD_CONF_SET "conf-set"
+#define CMD_CONF_UNSET "conf-unset"
+
+#define CTL_LOG_STR "failed to control"
+
+#define CTL_SEND(type, data) \
+ ret = knot_ctl_send(args->ctl, (type), (data)); \
+ if (ret != KNOT_EOK) { \
+ log_error(CTL_LOG_STR" (%s)", knot_strerror(ret)); \
+ return ret; \
+ }
+
+#define CTL_SEND_DATA CTL_SEND(KNOT_CTL_TYPE_DATA, &data)
+#define CTL_SEND_BLOCK CTL_SEND(KNOT_CTL_TYPE_BLOCK, NULL)
+
+static int check_args(cmd_args_t *args, int min, int max)
+{
+ if (max == 0 && args->argc > 0) {
+ log_error("command doesn't take arguments");
+ return KNOT_EINVAL;
+ } else if (min == max && args->argc != min) {
+ log_error("command requires %i arguments", min);
+ return KNOT_EINVAL;
+ } else if (args->argc < min) {
+ log_error("command requires at least %i arguments", min);
+ return KNOT_EINVAL;
+ } else if (max > 0 && args->argc > max) {
+ log_error("command takes at most %i arguments", max);
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static int check_conf_args(cmd_args_t *args)
+{
+ // Mask relevant flags.
+ cmd_flag_t flags = args->desc->flags;
+ flags &= CMD_FOPT_ITEM | CMD_FREQ_ITEM | CMD_FOPT_DATA;
+
+ switch (args->argc) {
+ case 0:
+ if (flags == CMD_FNONE || (flags & CMD_FOPT_ITEM)) {
+ return KNOT_EOK;
+ }
+ break;
+ case 1:
+ if (flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) {
+ return KNOT_EOK;
+ }
+ break;
+ default:
+ if (flags != CMD_FNONE) {
+ return KNOT_EOK;
+ }
+ break;
+ }
+
+ log_error("invalid number of arguments");
+
+ return KNOT_EINVAL;
+}
+
+static int get_conf_key(const char *key, knot_ctl_data_t *data)
+{
+ // Get key0.
+ const char *key0 = key;
+
+ // Check for id.
+ char *id = strchr(key, '[');
+ if (id != NULL) {
+ // Separate key0 and id.
+ *id++ = '\0';
+
+ // Check for id end.
+ char *id_end = id;
+ while ((id_end = strchr(id_end, ']')) != NULL) {
+ // Check for escaped character.
+ if (*(id_end - 1) != '\\') {
+ break;
+ }
+ id_end++;
+ }
+
+ // Check for unclosed id.
+ if (id_end == NULL) {
+ log_error("(missing bracket after identifier) %s", id);
+ return KNOT_EINVAL;
+ }
+
+ // Separate id and key1.
+ *id_end = '\0';
+
+ key = id_end + 1;
+
+ // Key1 or nothing must follow.
+ if (*key != '.' && *key != '\0') {
+ log_error("(unexpected token) %s", key);
+ return KNOT_EINVAL;
+ }
+ }
+
+ // Check for key1.
+ char *key1 = strchr(key, '.');
+ if (key1 != NULL) {
+ // Separate key0/id and key1.
+ *key1++ = '\0';
+
+ if (*key1 == '\0') {
+ log_error("(missing item specification)");
+ return KNOT_EINVAL;
+ }
+ }
+
+ (*data)[KNOT_CTL_IDX_SECTION] = key0;
+ (*data)[KNOT_CTL_IDX_ITEM] = key1;
+ (*data)[KNOT_CTL_IDX_ID] = id;
+
+ return KNOT_EOK;
+}
+
+static void format_data(cmd_args_t *args, knot_ctl_type_t data_type,
+ knot_ctl_data_t *data, bool *empty)
+{
+ const char *error = (*data)[KNOT_CTL_IDX_ERROR];
+ const char *flags = (*data)[KNOT_CTL_IDX_FLAGS];
+ const char *key0 = (*data)[KNOT_CTL_IDX_SECTION];
+ const char *key1 = (*data)[KNOT_CTL_IDX_ITEM];
+ const char *id = (*data)[KNOT_CTL_IDX_ID];
+ const char *zone = (*data)[KNOT_CTL_IDX_ZONE];
+ const char *owner = (*data)[KNOT_CTL_IDX_OWNER];
+ const char *ttl = (*data)[KNOT_CTL_IDX_TTL];
+ const char *type = (*data)[KNOT_CTL_IDX_TYPE];
+ const char *value = (*data)[KNOT_CTL_IDX_DATA];
+
+ bool col = false;
+ char status_col[32] = "";
+
+ static bool first_status_item = true;
+
+ const char *sign = NULL;
+ if (ctl_has_flag(flags, CTL_FLAG_DIFF_ADD)) {
+ sign = CTL_FLAG_DIFF_ADD;
+ } else if (ctl_has_flag(flags, CTL_FLAG_DIFF_REM)) {
+ sign = CTL_FLAG_DIFF_REM;
+ }
+
+ switch (args->desc->cmd) {
+ case CTL_STATUS:
+ if (error != NULL) {
+ printf("error: (%s)%s%s", error,
+ (type != NULL) ? " " : "",
+ (type != NULL) ? type : "");
+ } else if (value != NULL) {
+ printf("%s", value);
+ *empty = false;
+ }
+ break;
+ case CTL_STOP:
+ case CTL_RELOAD:
+ case CTL_CONF_BEGIN:
+ case CTL_CONF_ABORT:
+ // Only error message is expected here.
+ if (error != NULL) {
+ printf("error: (%s)", error);
+ }
+ break;
+ case CTL_ZONE_STATUS:
+ if (error == NULL) {
+ col = args->extended ? args->color_force : args->color;
+ }
+ if (!ctl_has_flag(flags, CTL_FLAG_STATUS_EMPTY)) {
+ strlcat(status_col, COL_BOLD(col), sizeof(status_col));
+ }
+ if (ctl_has_flag(flags, CTL_FLAG_STATUS_SLAVE)) {
+ strlcat(status_col, COL_RED(col), sizeof(status_col));
+ } else {
+ strlcat(status_col, COL_GRN(col), sizeof(status_col));
+ }
+ if (ctl_has_flag(flags, CTL_FLAG_STATUS_MEMBER)) {
+ strlcat(status_col, COL_UNDR(col), sizeof(status_col));
+ }
+ // FALLTHROUGH
+ case CTL_ZONE_RELOAD:
+ case CTL_ZONE_REFRESH:
+ case CTL_ZONE_RETRANSFER:
+ case CTL_ZONE_NOTIFY:
+ case CTL_ZONE_FLUSH:
+ case CTL_ZONE_BACKUP:
+ case CTL_ZONE_RESTORE:
+ case CTL_ZONE_SIGN:
+ case CTL_ZONE_KEYS_LOAD:
+ case CTL_ZONE_KEY_ROLL:
+ case CTL_ZONE_KSK_SBM:
+ case CTL_ZONE_FREEZE:
+ case CTL_ZONE_THAW:
+ case CTL_ZONE_BEGIN:
+ case CTL_ZONE_COMMIT:
+ case CTL_ZONE_ABORT:
+ case CTL_ZONE_PURGE:
+ if (data_type == KNOT_CTL_TYPE_DATA) {
+ printf("%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: " : ""),
+ (zone != NULL ? "[" : ""),
+ (zone != NULL ? status_col : ""),
+ (zone != NULL ? zone : ""),
+ (zone != NULL ? COL_RST(col) : ""),
+ (zone != NULL ? "]" : ""),
+ (error != NULL ? " (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ")" : ""));
+ *empty = false;
+ }
+ if (args->desc->cmd == CTL_ZONE_STATUS && type != NULL) {
+ if (data_type == KNOT_CTL_TYPE_DATA) {
+ first_status_item = true;
+ }
+ if (!args->extended &&
+ (value == 0 || strcmp(value, STATUS_EMPTY) == 0) &&
+ strcmp(type, "serial") != 0) {
+ return;
+ }
+
+ printf("%s %s: %s%s%s",
+ (first_status_item ? "" : " |"),
+ type, COL_BOLD(col), value, COL_RST(col));
+ first_status_item = false;
+ }
+ break;
+ case CTL_CONF_COMMIT: // Can return a check error context.
+ case CTL_CONF_LIST:
+ case CTL_CONF_READ:
+ case CTL_CONF_DIFF:
+ case CTL_CONF_GET:
+ case CTL_CONF_SET:
+ case CTL_CONF_UNSET:
+ if (data_type == KNOT_CTL_TYPE_DATA) {
+ printf("%s%s%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ") " : ""),
+ (sign != NULL ? sign : ""),
+ (key0 != NULL ? key0 : ""),
+ (id != NULL ? "[" : ""),
+ (id != NULL ? id : ""),
+ (id != NULL ? "]" : ""),
+ (key1 != NULL ? "." : ""),
+ (key1 != NULL ? key1 : ""),
+ (value != NULL ? " =" : ""));
+ *empty = false;
+ }
+ if (value != NULL) {
+ printf(" %s", value);
+ }
+ break;
+ case CTL_ZONE_READ:
+ case CTL_ZONE_DIFF:
+ case CTL_ZONE_GET:
+ case CTL_ZONE_SET:
+ case CTL_ZONE_UNSET:
+ printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ") " : ""),
+ (zone != NULL ? "[" : ""),
+ (zone != NULL ? zone : ""),
+ (zone != NULL ? "] " : ""),
+ (sign != NULL ? sign : ""),
+ (owner != NULL ? owner : ""),
+ (ttl != NULL ? " " : ""),
+ (ttl != NULL ? ttl : ""),
+ (type != NULL ? " " : ""),
+ (type != NULL ? type : ""),
+ (value != NULL ? " " : ""),
+ (value != NULL ? value : ""));
+ *empty = false;
+ break;
+ case CTL_STATS:
+ case CTL_ZONE_STATS:
+ printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
+ (!(*empty) ? "\n" : ""),
+ (error != NULL ? "error: (" : ""),
+ (error != NULL ? error : ""),
+ (error != NULL ? ") " : ""),
+ (zone != NULL ? "[" : ""),
+ (zone != NULL ? zone : ""),
+ (zone != NULL ? "] " : ""),
+ (key0 != NULL ? key0 : ""),
+ (key1 != NULL ? "." : ""),
+ (key1 != NULL ? key1 : ""),
+ (id != NULL ? "[" : ""),
+ (id != NULL ? id : ""),
+ (id != NULL ? "]" : ""),
+ (value != NULL ? " = " : ""),
+ (value != NULL ? value : ""));
+ *empty = false;
+ break;
+ default:
+ assert(0);
+ }
+}
+
+static void format_block(ctl_cmd_t cmd, bool failed, bool empty)
+{
+ switch (cmd) {
+ case CTL_STATUS:
+ printf("%s\n", (failed || !empty) ? "" : "Running");
+ break;
+ case CTL_STOP:
+ printf("%s\n", failed ? "" : "Stopped");
+ break;
+ case CTL_RELOAD:
+ printf("%s\n", failed ? "" : "Reloaded");
+ break;
+ case CTL_CONF_BEGIN:
+ case CTL_CONF_COMMIT:
+ case CTL_CONF_ABORT:
+ case CTL_CONF_SET:
+ case CTL_CONF_UNSET:
+ case CTL_ZONE_RELOAD:
+ case CTL_ZONE_REFRESH:
+ case CTL_ZONE_RETRANSFER:
+ case CTL_ZONE_NOTIFY:
+ case CTL_ZONE_FLUSH:
+ case CTL_ZONE_BACKUP:
+ case CTL_ZONE_RESTORE:
+ case CTL_ZONE_SIGN:
+ case CTL_ZONE_KEYS_LOAD:
+ case CTL_ZONE_KEY_ROLL:
+ case CTL_ZONE_KSK_SBM:
+ case CTL_ZONE_FREEZE:
+ case CTL_ZONE_THAW:
+ case CTL_ZONE_XFR_FREEZE:
+ case CTL_ZONE_XFR_THAW:
+ case CTL_ZONE_BEGIN:
+ case CTL_ZONE_COMMIT:
+ case CTL_ZONE_ABORT:
+ case CTL_ZONE_SET:
+ case CTL_ZONE_UNSET:
+ case CTL_ZONE_PURGE:
+ printf("%s\n", failed ? "" : "OK");
+ break;
+ case CTL_ZONE_STATUS:
+ case CTL_ZONE_READ:
+ case CTL_ZONE_DIFF:
+ case CTL_ZONE_GET:
+ case CTL_CONF_LIST:
+ case CTL_CONF_READ:
+ case CTL_CONF_DIFF:
+ case CTL_CONF_GET:
+ case CTL_ZONE_STATS:
+ case CTL_STATS:
+ printf("%s", empty ? "" : "\n");
+ break;
+ default:
+ assert(0);
+ }
+}
+
+static int ctl_receive(cmd_args_t *args)
+{
+ bool failed = false;
+ bool empty = true;
+
+ while (true) {
+ knot_ctl_type_t type;
+ knot_ctl_data_t data;
+
+ int ret = knot_ctl_receive(args->ctl, &type, &data);
+ if (ret != KNOT_EOK) {
+ log_error(CTL_LOG_STR" (%s)", knot_strerror(ret));
+ return ret;
+ }
+
+ switch (type) {
+ case KNOT_CTL_TYPE_END:
+ log_error(CTL_LOG_STR" (%s)", knot_strerror(KNOT_EMALF));
+ return KNOT_EMALF;
+ case KNOT_CTL_TYPE_BLOCK:
+ format_block(args->desc->cmd, failed, empty);
+ return failed ? KNOT_ERROR : KNOT_EOK;
+ case KNOT_CTL_TYPE_DATA:
+ case KNOT_CTL_TYPE_EXTRA:
+ format_data(args, type, &data, &empty);
+ break;
+ default:
+ assert(0);
+ return KNOT_EINVAL;
+ }
+
+ if (data[KNOT_CTL_IDX_ERROR] != NULL) {
+ failed = true;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int cmd_ctl(cmd_args_t *args)
+{
+ int ret = check_args(args, 0, (args->desc->cmd == CTL_STATUS ? 1 : 0));
+ 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,
+ [KNOT_CTL_IDX_TYPE] = args->argc > 0 ? args->argv[0] : NULL
+ };
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int set_stats_items(cmd_args_t *args, knot_ctl_data_t *data)
+{
+ int min_args, max_args;
+ switch (args->desc->cmd) {
+ case CTL_STATS: min_args = 0; max_args = 1; break;
+ case CTL_ZONE_STATS: min_args = 1; max_args = 2; break;
+ default:
+ assert(0);
+ return KNOT_EINVAL;
+ }
+
+ // Check the number of arguments.
+ int ret = check_args(args, min_args, max_args);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ int idx = 0;
+
+ // Set ZONE name.
+ if (args->argc > idx && args->desc->cmd == CTL_ZONE_STATS) {
+ if (strcmp(args->argv[idx], "--") != 0) {
+ (*data)[KNOT_CTL_IDX_ZONE] = args->argv[idx];
+ }
+ idx++;
+ }
+
+ if (args->argc > idx) {
+ (*data)[KNOT_CTL_IDX_SECTION] = args->argv[idx];
+
+ char *item = strchr(args->argv[idx], '.');
+ if (item != NULL) {
+ // Separate section and item.
+ *item++ = '\0';
+ (*data)[KNOT_CTL_IDX_ITEM] = item;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int cmd_stats_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ int ret = set_stats_items(args, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int zone_exec(cmd_args_t *args, int (*fcn)(const knot_dname_t *, void *),
+ void *data)
+{
+ bool failed = false;
+
+ // Process specified zones.
+ if (args->argc > 0) {
+ knot_dname_storage_t id;
+
+ for (int i = 0; i < args->argc; i++) {
+ if (knot_dname_from_str(id, args->argv[i], sizeof(id)) == NULL) {
+ log_zone_str_error(args->argv[i], "invalid name");
+ failed = true;
+ continue;
+ }
+ knot_dname_to_lower(id);
+
+ if (!conf_rawid_exists(conf(), C_ZONE, id, knot_dname_size(id))) {
+ log_zone_error(id, "%s", knot_strerror(KNOT_ENOZONE));
+ failed = true;
+ continue;
+ }
+
+ if (fcn(id, data) != KNOT_EOK) {
+ failed = true;
+ }
+ }
+ // Process all configured zones.
+ } else {
+ for (conf_iter_t iter = conf_iter(conf(), C_ZONE);
+ iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) {
+ conf_val_t val = conf_iter_id(conf(), &iter);
+ const knot_dname_t *id = conf_dname(&val);
+
+ if (fcn(id, data) != KNOT_EOK) {
+ failed = true;
+ }
+ }
+ }
+
+ return failed ? KNOT_ERROR : KNOT_EOK;
+}
+
+static int zone_check(const knot_dname_t *dname, void *data)
+{
+ cmd_args_t *args = data;
+
+ zone_contents_t *contents = NULL;
+ conf_val_t mode = conf_zone_get(conf(), C_SEM_CHECKS, dname);
+ int ret = zone_load_contents(conf(), dname, &contents, conf_opt(&mode), args->force);
+ zone_contents_deep_free(contents);
+ if (ret != KNOT_EOK && ret != KNOT_ESEMCHECK) {
+ knot_dname_txt_storage_t name;
+ (void)knot_dname_to_str(name, dname, sizeof(name));
+ log_error("[%s] failed to check zone file (%s)", name, knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int cmd_zone_check(cmd_args_t *args)
+{
+ return zone_exec(args, zone_check, args);
+}
+
+static int cmd_zone_key_roll_ctl(cmd_args_t *args)
+{
+ int ret = check_args(args, 2, 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,
+ [KNOT_CTL_IDX_ZONE] = args->argv[0],
+ [KNOT_CTL_IDX_TYPE] = args->argv[1],
+ };
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int cmd_zone_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ // Check the number of arguments.
+ int ret = check_args(args, (args->desc->flags & CMD_FREQ_ZONE) ? 1 : 0, -1);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (args->desc->cmd == CTL_ZONE_PURGE && !args->force) {
+ log_error("force option required!");
+ return KNOT_EDENIED;
+ }
+
+ // Ignore all zones argument.
+ if (args->argc == 1 && strcmp(args->argv[0], "--") == 0) {
+ args->argc = 0;
+ }
+
+ if (args->argc == 0) {
+ CTL_SEND_DATA
+ }
+ for (int i = 0; i < args->argc; i++) {
+ data[KNOT_CTL_IDX_ZONE] = args->argv[i];
+
+ CTL_SEND_DATA
+ }
+
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+#define MAX_FILTERS 12
+
+typedef struct {
+ const char *name;
+ char id;
+ bool with_data; // Only ONE filter of each filter_desc_t may have data!
+} filter_desc_t;
+
+const filter_desc_t zone_flush_filters[MAX_FILTERS] = {
+ { "+outdir", CTL_FILTER_FLUSH_OUTDIR, true },
+};
+
+const filter_desc_t zone_backup_filters[MAX_FILTERS] = {
+ { "+backupdir", CTL_FILTER_BACKUP_OUTDIR, true },
+ { "+zonefile", CTL_FILTER_BACKUP_ZONEFILE, false },
+ { "+nozonefile", CTL_FILTER_BACKUP_NOZONEFILE, false },
+ { "+journal", CTL_FILTER_BACKUP_JOURNAL, false },
+ { "+nojournal", CTL_FILTER_BACKUP_NOJOURNAL, false },
+ { "+timers", CTL_FILTER_BACKUP_TIMERS, false },
+ { "+notimers", CTL_FILTER_BACKUP_NOTIMERS, false },
+ { "+kaspdb", CTL_FILTER_BACKUP_KASPDB, false },
+ { "+nokaspdb", CTL_FILTER_BACKUP_NOKASPDB, false },
+ { "+catalog", CTL_FILTER_BACKUP_CATALOG, false },
+ { "+nocatalog", CTL_FILTER_BACKUP_NOCATALOG, false },
+};
+
+const filter_desc_t zone_status_filters[MAX_FILTERS] = {
+ { "+role", CTL_FILTER_STATUS_ROLE },
+ { "+serial", CTL_FILTER_STATUS_SERIAL },
+ { "+transaction", CTL_FILTER_STATUS_TRANSACTION },
+ { "+freeze", CTL_FILTER_STATUS_FREEZE },
+ { "+catalog", CTL_FILTER_STATUS_CATALOG },
+ { "+events", CTL_FILTER_STATUS_EVENTS },
+};
+
+const filter_desc_t zone_purge_filters[MAX_FILTERS] = {
+ { "+expire", CTL_FILTER_PURGE_EXPIRE },
+ { "+zonefile", CTL_FILTER_PURGE_ZONEFILE },
+ { "+journal", CTL_FILTER_PURGE_JOURNAL },
+ { "+timers", CTL_FILTER_PURGE_TIMERS },
+ { "+kaspdb", CTL_FILTER_PURGE_KASPDB },
+ { "+catalog", CTL_FILTER_PURGE_CATALOG },
+ { "+orphan", CTL_FILTER_PURGE_ORPHAN },
+};
+
+const filter_desc_t null_filter = { 0 };
+
+static const filter_desc_t *get_filter(ctl_cmd_t cmd, const char *filter_name)
+{
+ const filter_desc_t *fd = NULL;
+ switch (cmd) {
+ case CTL_ZONE_FLUSH:
+ fd = zone_flush_filters;
+ break;
+ case CTL_ZONE_BACKUP:
+ case CTL_ZONE_RESTORE:
+ fd = zone_backup_filters;
+ break;
+ case CTL_ZONE_STATUS:
+ fd = zone_status_filters;
+ break;
+ case CTL_ZONE_PURGE:
+ fd = zone_purge_filters;
+ break;
+ default:
+ return &null_filter;
+ }
+ for (size_t i = 0; i < MAX_FILTERS && fd[i].name != NULL; i++) {
+ if (strcmp(fd[i].name, filter_name) == 0) {
+ return &fd[i];
+ }
+ }
+ return &null_filter;
+}
+
+static int cmd_zone_filter_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ if (args->desc->cmd == CTL_ZONE_PURGE && !args->force) {
+ log_error("force option required!");
+ return KNOT_EDENIED;
+ }
+
+ char filter_buff[MAX_FILTERS + 1] = { 0 };
+
+ // First, process the filters.
+ for (int i = 0; i < args->argc; i++) {
+ if (args->argv[i][0] == '+') {
+ if (data[KNOT_CTL_IDX_FILTER] == NULL) {
+ data[KNOT_CTL_IDX_FILTER] = filter_buff;
+ }
+ char filter_id[2] = { get_filter(args->desc->cmd, args->argv[i])->id, 0 };
+ if (filter_id[0] == '\0') {
+ log_error("unknown filter: %s", args->argv[i]);
+ return KNOT_EINVAL;
+ }
+ if (strchr(filter_buff, filter_id[0]) == NULL) {
+ assert(strlen(filter_buff) < MAX_FILTERS);
+ strlcat(filter_buff, filter_id, sizeof(filter_buff));
+ }
+ if (get_filter(args->desc->cmd, args->argv[i])->with_data) {
+ data[KNOT_CTL_IDX_DATA] = args->argv[++i];
+ }
+ }
+ }
+
+ // Second, process zones.
+ int ret;
+ int sentzones = 0;
+ bool twodash = false;
+ for (int i = 0; i < args->argc; i++) {
+ // Skip filters.
+ if (args->argv[i][0] == '+') {
+ if (get_filter(args->desc->cmd, args->argv[i])->with_data) {
+ i++;
+ }
+ continue;
+ }
+
+ if (strcmp(args->argv[i], "--") != 0) {
+ data[KNOT_CTL_IDX_ZONE] = args->argv[i];
+ CTL_SEND_DATA
+ sentzones++;
+ } else {
+ twodash = true;
+ }
+ }
+
+ if ((args->desc->flags & CMD_FREQ_ZONE) && sentzones == 0 && !twodash) {
+ log_error("zone must be specified (or -- for all zones)");
+ return KNOT_EDENIED;
+ }
+
+ if (sentzones == 0) {
+ CTL_SEND_DATA
+ }
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int set_rdata(cmd_args_t *args, int pos, char *rdata, size_t rdata_len)
+{
+ rdata[0] = '\0';
+
+ for (int i = pos; i < args->argc; i++) {
+ if (i > pos && strlcat(rdata, " ", rdata_len) >= rdata_len) {
+ return KNOT_ESPACE;
+ }
+ if (strlcat(rdata, args->argv[i], rdata_len) >= rdata_len) {
+ return KNOT_ESPACE;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int set_node_items(cmd_args_t *args, knot_ctl_data_t *data, char *rdata,
+ size_t rdata_len)
+{
+ int min_args, max_args;
+ switch (args->desc->cmd) {
+ case CTL_ZONE_READ:
+ case CTL_ZONE_GET: min_args = 1; max_args = 3; break;
+ case CTL_ZONE_DIFF: min_args = 1; max_args = 1; break;
+ case CTL_ZONE_SET: min_args = 3; max_args = -1; break;
+ case CTL_ZONE_UNSET: min_args = 2; max_args = -1; break;
+ default:
+ assert(0);
+ return KNOT_EINVAL;
+ }
+
+ // Check the number of arguments.
+ int ret = check_args(args, min_args, max_args);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ int idx = 0;
+
+ // Set ZONE name.
+ assert(args->argc > idx);
+ if (strcmp(args->argv[idx], "--") != 0) {
+ (*data)[KNOT_CTL_IDX_ZONE] = args->argv[idx];
+ }
+ idx++;
+
+ // Set OWNER name if specified.
+ if (args->argc > idx) {
+ (*data)[KNOT_CTL_IDX_OWNER] = args->argv[idx];
+ idx++;
+ }
+
+ // Set TTL only with an editing operation.
+ if (args->argc > idx) {
+ uint32_t num;
+ uint16_t type;
+ if (knot_rrtype_from_string(args->argv[idx], &type) != 0 &&
+ str_to_u32(args->argv[idx], &num) == KNOT_EOK) {
+ switch (args->desc->cmd) {
+ case CTL_ZONE_SET:
+ case CTL_ZONE_UNSET:
+ (*data)[KNOT_CTL_IDX_TTL] = args->argv[idx];
+ idx++;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // Set record TYPE if specified.
+ if (args->argc > idx) {
+ (*data)[KNOT_CTL_IDX_TYPE] = args->argv[idx];
+ idx++;
+ }
+
+ // Set record DATA if specified.
+ if (args->argc > idx) {
+ ret = set_rdata(args, idx, rdata, rdata_len);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ (*data)[KNOT_CTL_IDX_DATA] = rdata;
+ }
+
+ return KNOT_EOK;
+}
+
+static int cmd_zone_node_ctl(cmd_args_t *args)
+{
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = args->flags,
+ };
+
+ char rdata[65536]; // Maximum item size in libknot control interface.
+
+ int ret = set_node_items(args, &data, rdata, sizeof(rdata));
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ CTL_SEND_DATA
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+static int cmd_conf_init(cmd_args_t *args)
+{
+ int ret = check_args(args, 0, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = conf_db_check(conf(), &conf()->read_txn);
+ if ((ret >= KNOT_EOK || ret == KNOT_CONF_EVERSION)) {
+ if (ret != KNOT_EOK && !args->force) {
+ log_error("use force option to overwrite the existing "
+ "destination and ensure the server is not running!");
+ return KNOT_EDENIED;
+ }
+
+ ret = conf_import(conf(), "", false, false);
+ }
+
+ if (ret == KNOT_EOK) {
+ log_info("OK");
+ } else {
+ log_error("init (%s)", knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int conf_check_group(const yp_item_t *group, const uint8_t *id, size_t id_len)
+{
+ knotd_conf_check_extra_t extra = {
+ .conf = conf(),
+ .txn = &conf()->read_txn,
+ .check = true
+ };
+ knotd_conf_check_args_t args = {
+ .id = id,
+ .id_len = id_len,
+ .extra = &extra
+ };
+
+ bool non_empty = false;
+ bool error = false;
+
+ // Check the group sub-items.
+ for (yp_item_t *item = group->sub_items; item->name != NULL; item++) {
+ args.item = item;
+
+ conf_val_t bin;
+ conf_db_get(conf(), &conf()->read_txn, group->name, item->name,
+ id, id_len, &bin);
+ if (bin.code == KNOT_ENOENT) {
+ continue;
+ } else if (bin.code != KNOT_EOK) {
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(bin.code));
+ return bin.code;
+ }
+
+ non_empty = true;
+
+ // Check the item value(s).
+ size_t values = conf_val_count(&bin);
+ for (size_t i = 1; i <= values; i++) {
+ conf_val(&bin);
+ args.data = bin.data;
+ args.data_len = bin.len;
+
+ int ret = conf_exec_callbacks(&args);
+ if (ret != KNOT_EOK) {
+ log_error("config, item '%s%s%s%s.%s' (%s)",
+ group->name + 1,
+ (id != NULL ? "[" : ""),
+ (id != NULL ? (const char *)id : ""),
+ (id != NULL ? "]" : ""),
+ item->name + 1,
+ args.err_str);
+ error = true;
+ }
+ if (values > 1) {
+ conf_val_next(&bin);
+ }
+ }
+ }
+
+ // Check the group item itself.
+ if (id != NULL || non_empty) {
+ args.item = group;
+ args.data = NULL;
+ args.data_len = 0;
+
+ int ret = conf_exec_callbacks(&args);
+ if (ret != KNOT_EOK) {
+ log_error("config, section '%s%s%s%s' (%s)",
+ group->name + 1,
+ (id != NULL ? "[" : ""),
+ (id != NULL ? (const char *)id : ""),
+ (id != NULL ? "]" : ""),
+ args.err_str);
+ error = true;
+ }
+ }
+
+ return error ? KNOT_ESEMCHECK : KNOT_EOK;
+}
+
+static int cmd_conf_check(cmd_args_t *args) // Similar to conf_io_check().
+{
+ int ret = check_args(args, 0, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (conf()->filename == NULL) { // Config file already checked.
+ for (yp_item_t *item = conf()->schema; item->name != NULL; item++) {
+ // Skip include item.
+ if (item->type != YP_TGRP) {
+ continue;
+ }
+
+ // Group without identifiers.
+ if (!(item->flags & YP_FMULTI)) {
+ ret = conf_check_group(item, NULL, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ continue;
+ }
+
+ conf_iter_t iter;
+ ret = conf_db_iter_begin(conf(), &conf()->read_txn, item->name, &iter);
+ if (ret == KNOT_ENOENT) {
+ continue;
+ } else if (ret != KNOT_EOK) {
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ while (ret == KNOT_EOK) {
+ const uint8_t *id;
+ size_t id_len;
+ ret = conf_db_iter_id(conf(), &iter, &id, &id_len);
+ if (ret != KNOT_EOK) {
+ conf_db_iter_finish(conf(), &iter);
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ // Check the group with this identifier.
+ ret = conf_check_group(item, id, id_len);
+ if (ret != KNOT_EOK) {
+ conf_db_iter_finish(conf(), &iter);
+ return ret;
+ }
+
+ ret = conf_db_iter_next(conf(), &iter);
+ }
+ if (ret != KNOT_EOF) {
+ log_error("failed to read the configuration DB (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+ }
+ }
+
+ log_info("Configuration is valid");
+
+ return KNOT_EOK;
+}
+
+static int cmd_conf_import(cmd_args_t *args)
+{
+ int ret = check_args(args, 1, 1);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = conf_db_check(conf(), &conf()->read_txn);
+ if ((ret >= KNOT_EOK || ret == KNOT_CONF_EVERSION)) {
+ if (ret != KNOT_EOK && !args->force) {
+ log_error("use force option to overwrite the existing "
+ "destination and ensure the server is not running!");
+ return KNOT_EDENIED;
+ }
+
+ log_debug("importing confdb from file '%s'", args->argv[0]);
+
+ ret = conf_import(conf(), args->argv[0], true, false);
+ }
+
+ if (ret == KNOT_EOK) {
+ log_info("OK");
+ } else {
+ log_error("import (%s)", knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int cmd_conf_export(cmd_args_t *args)
+{
+ int ret = check_args(args, 0, 1);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Stdout is the default output file.
+ const char *file_name = NULL;
+ if (args->argc > 0) {
+ file_name = args->argv[0];
+ log_debug("exporting confdb into file '%s'", file_name);
+ }
+
+ ret = conf_export(conf(), file_name, YP_SNONE);
+
+ if (ret == KNOT_EOK) {
+ if (args->argc > 0) {
+ log_info("OK");
+ }
+ } else {
+ log_error("export (%s)", knot_strerror(ret));
+ }
+
+ return ret;
+}
+
+static int cmd_conf_ctl(cmd_args_t *args)
+{
+ // Check the number of arguments.
+ int ret = check_conf_args(args);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ char flags[16] = "";
+ strlcat(flags, args->flags, sizeof(flags));
+ if (args->desc->flags & CMD_FLIST_SCHEMA) {
+ strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags));
+ }
+
+ knot_ctl_data_t data = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(args->desc->cmd),
+ [KNOT_CTL_IDX_FLAGS] = flags,
+ };
+
+ // Send the command without parameters.
+ if (args->argc == 0) {
+ CTL_SEND_DATA
+ // Set the first item argument.
+ } else {
+ ret = get_conf_key(args->argv[0], &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Send if only one argument or item without values.
+ if (args->argc == 1 || !(args->desc->flags & CMD_FOPT_DATA)) {
+ CTL_SEND_DATA
+ }
+ }
+
+ // Send the item values or the other items.
+ for (int i = 1; i < args->argc; i++) {
+ if (args->desc->flags & CMD_FOPT_DATA) {
+ data[KNOT_CTL_IDX_DATA] = args->argv[i];
+ } else {
+ ret = get_conf_key(args->argv[i], &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ CTL_SEND_DATA
+ }
+
+ CTL_SEND_BLOCK
+
+ return ctl_receive(args);
+}
+
+const cmd_desc_t cmd_table[] = {
+ { CMD_EXIT, NULL, CTL_NONE },
+
+ { CMD_STATUS, cmd_ctl, CTL_STATUS, CMD_FOPT_DATA},
+ { CMD_STOP, cmd_ctl, CTL_STOP },
+ { CMD_RELOAD, cmd_ctl, CTL_RELOAD },
+ { CMD_STATS, cmd_stats_ctl, CTL_STATS },
+
+ { CMD_ZONE_CHECK, cmd_zone_check, CTL_NONE, CMD_FOPT_ZONE | CMD_FREAD },
+ { CMD_ZONE_STATUS, cmd_zone_filter_ctl, CTL_ZONE_STATUS, CMD_FOPT_ZONE },
+ { CMD_ZONE_RELOAD, cmd_zone_ctl, CTL_ZONE_RELOAD, CMD_FOPT_ZONE },
+ { CMD_ZONE_REFRESH, cmd_zone_ctl, CTL_ZONE_REFRESH, CMD_FOPT_ZONE },
+ { CMD_ZONE_RETRANSFER, cmd_zone_ctl, CTL_ZONE_RETRANSFER, CMD_FOPT_ZONE },
+ { CMD_ZONE_NOTIFY, cmd_zone_ctl, CTL_ZONE_NOTIFY, CMD_FOPT_ZONE },
+ { CMD_ZONE_FLUSH, cmd_zone_filter_ctl, CTL_ZONE_FLUSH, CMD_FOPT_ZONE },
+ { CMD_ZONE_BACKUP, cmd_zone_filter_ctl, CTL_ZONE_BACKUP, CMD_FOPT_ZONE },
+ { CMD_ZONE_RESTORE, cmd_zone_filter_ctl, CTL_ZONE_RESTORE, CMD_FOPT_ZONE },
+ { CMD_ZONE_SIGN, cmd_zone_ctl, CTL_ZONE_SIGN, CMD_FOPT_ZONE },
+ { CMD_ZONE_KEYS_LOAD, cmd_zone_ctl, CTL_ZONE_KEYS_LOAD, CMD_FOPT_ZONE },
+ { CMD_ZONE_KEY_ROLL, cmd_zone_key_roll_ctl, CTL_ZONE_KEY_ROLL, CMD_FREQ_ZONE },
+ { CMD_ZONE_KSK_SBM, cmd_zone_ctl, CTL_ZONE_KSK_SBM, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_FREEZE, cmd_zone_ctl, CTL_ZONE_FREEZE, CMD_FOPT_ZONE },
+ { CMD_ZONE_THAW, cmd_zone_ctl, CTL_ZONE_THAW, CMD_FOPT_ZONE },
+ { CMD_ZONE_XFR_FREEZE, cmd_zone_ctl, CTL_ZONE_XFR_FREEZE, CMD_FOPT_ZONE },
+ { CMD_ZONE_XFR_THAW, cmd_zone_ctl, CTL_ZONE_XFR_THAW, CMD_FOPT_ZONE },
+
+ { CMD_ZONE_READ, cmd_zone_node_ctl, CTL_ZONE_READ, CMD_FREQ_ZONE },
+ { CMD_ZONE_BEGIN, cmd_zone_ctl, CTL_ZONE_BEGIN, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_COMMIT, cmd_zone_ctl, CTL_ZONE_COMMIT, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_ABORT, cmd_zone_ctl, CTL_ZONE_ABORT, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_DIFF, cmd_zone_node_ctl, CTL_ZONE_DIFF, CMD_FREQ_ZONE },
+ { CMD_ZONE_GET, cmd_zone_node_ctl, CTL_ZONE_GET, CMD_FREQ_ZONE },
+ { CMD_ZONE_SET, cmd_zone_node_ctl, CTL_ZONE_SET, CMD_FREQ_ZONE },
+ { CMD_ZONE_UNSET, cmd_zone_node_ctl, CTL_ZONE_UNSET, CMD_FREQ_ZONE },
+ { CMD_ZONE_PURGE, cmd_zone_filter_ctl, CTL_ZONE_PURGE, CMD_FREQ_ZONE | CMD_FOPT_ZONE },
+ { CMD_ZONE_STATS, cmd_stats_ctl, CTL_ZONE_STATS, 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_CONF_IMPORT, cmd_conf_import, CTL_NONE, CMD_FWRITE | CMD_FOPT_MOD },
+ { CMD_CONF_EXPORT, cmd_conf_export, CTL_NONE, CMD_FREAD | CMD_FOPT_MOD },
+ { CMD_CONF_LIST, cmd_conf_ctl, CTL_CONF_LIST, CMD_FOPT_ITEM | CMD_FLIST_SCHEMA },
+ { CMD_CONF_READ, cmd_conf_ctl, CTL_CONF_READ, CMD_FOPT_ITEM },
+ { CMD_CONF_BEGIN, cmd_conf_ctl, CTL_CONF_BEGIN },
+ { CMD_CONF_COMMIT, cmd_conf_ctl, CTL_CONF_COMMIT },
+ { CMD_CONF_ABORT, cmd_conf_ctl, CTL_CONF_ABORT },
+ { CMD_CONF_DIFF, cmd_conf_ctl, CTL_CONF_DIFF, CMD_FOPT_ITEM | CMD_FREQ_TXN },
+ { CMD_CONF_GET, cmd_conf_ctl, CTL_CONF_GET, CMD_FOPT_ITEM | CMD_FREQ_TXN },
+ { CMD_CONF_SET, cmd_conf_ctl, CTL_CONF_SET, CMD_FREQ_ITEM | CMD_FOPT_DATA | CMD_FREQ_TXN | CMD_FLIST_SCHEMA},
+ { CMD_CONF_UNSET, cmd_conf_ctl, CTL_CONF_UNSET, CMD_FOPT_ITEM | CMD_FOPT_DATA | CMD_FREQ_TXN },
+ { NULL }
+};
+
+static const cmd_help_t cmd_help_table[] = {
+ { CMD_EXIT, "", "Exit interactive mode." },
+ { "", "", "" },
+ { CMD_STATUS, "[<detail>]", "Check if the server is running." },
+ { CMD_STOP, "", "Stop the server if running." },
+ { CMD_RELOAD, "", "Reload the server configuration and modified zones." },
+ { CMD_STATS, "[<module>[.<counter>]]", "Show global statistics counter(s)." },
+ { "", "", "" },
+ { CMD_ZONE_CHECK, "[<zone>...]", "Check if the zone can be loaded. (*)" },
+ { CMD_ZONE_STATUS, "[<zone>...] [<filter>...]", "Show the zone status." },
+ { CMD_ZONE_RELOAD, "[<zone>...]", "Reload a zone from a disk. (#)" },
+ { CMD_ZONE_REFRESH, "[<zone>...]", "Force slave zone refresh. (#)" },
+ { CMD_ZONE_NOTIFY, "[<zone>...]", "Send a NOTIFY message to all configured remotes. (#)" },
+ { CMD_ZONE_RETRANSFER, "[<zone>...]", "Force slave zone retransfer (no serial check). (#)" },
+ { CMD_ZONE_FLUSH, "[<zone>...] [<filter>...]", "Flush zone journal into the zone file. (#)" },
+ { CMD_ZONE_BACKUP, "[<zone>...] [<filter>...] +backupdir <dir>", "Backup zone data and metadata. (#)" },
+ { CMD_ZONE_RESTORE, "[<zone>...] [<filter>...] +backupdir <dir>", "Restore zone data and metadata. (#)" },
+ { CMD_ZONE_SIGN, "[<zone>...]", "Re-sign the automatically signed zone. (#)" },
+ { CMD_ZONE_KEYS_LOAD, "[<zone>...]", "Re-load keys from KASP database, sign the zone. (#)" },
+ { CMD_ZONE_KEY_ROLL, " <zone> ksk|zsk", "Trigger immediate key rollover. (#)" },
+ { CMD_ZONE_KSK_SBM, " <zone>...", "When KSK submission, confirm parent's DS presence. (#)" },
+ { CMD_ZONE_FREEZE, "[<zone>...]", "Temporarily postpone automatic zone-changing events. (#)" },
+ { CMD_ZONE_THAW, "[<zone>...]", "Dismiss zone freeze. (#)" },
+ { CMD_ZONE_XFR_FREEZE, "[<zone>...]", "Temporarily disable outgoing AXFR/IXFR. (#)" },
+ { CMD_ZONE_XFR_THAW, "[<zone>...]", "Dismiss outgoing XFR freeze. (#)" },
+ { "", "", "" },
+ { CMD_ZONE_READ, "<zone> [<owner> [<type>]]", "Get zone data that are currently being presented." },
+ { CMD_ZONE_BEGIN, "<zone>...", "Begin a zone transaction." },
+ { CMD_ZONE_COMMIT, "<zone>...", "Commit the zone transaction." },
+ { CMD_ZONE_ABORT, "<zone>...", "Abort the zone transaction." },
+ { CMD_ZONE_DIFF, "<zone>", "Get zone changes within the transaction." },
+ { CMD_ZONE_GET, "<zone> [<owner> [<type>]]", "Get zone data within the transaction." },
+ { CMD_ZONE_SET, "<zone> <owner> [<ttl>] <type> <rdata>", "Add zone record within the transaction." },
+ { 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_CONF_INIT, "", "Initialize the confdb. (*)" },
+ { CMD_CONF_CHECK, "", "Check the server configuration. (*)" },
+ { CMD_CONF_IMPORT, " <filename>", "Import a config file into the confdb. (*)" },
+ { CMD_CONF_EXPORT, "[<filename>]", "Export the confdb into a config file or stdout. (*)" },
+ { CMD_CONF_LIST, "[<item>...]", "List the confdb sections or section items." },
+ { CMD_CONF_READ, "[<item>...]", "Get the item from the active confdb." },
+ { CMD_CONF_BEGIN, "", "Begin a writing confdb transaction." },
+ { CMD_CONF_COMMIT, "", "Commit the confdb transaction." },
+ { CMD_CONF_ABORT, "", "Rollback the confdb transaction." },
+ { CMD_CONF_DIFF, "[<item>...]", "Get the item difference within the transaction." },
+ { CMD_CONF_GET, "[<item>...]", "Get the item data within the transaction." },
+ { CMD_CONF_SET, " <item> [<data>...]", "Set the item data within the transaction." },
+ { CMD_CONF_UNSET, "[<item>] [<data>...]", "Unset the item data within the transaction." },
+ { NULL }
+};
+
+void print_commands(void)
+{
+ printf("\nActions:\n");
+
+ for (const cmd_help_t *cmd = cmd_help_table; cmd->name != NULL; cmd++) {
+ printf(" %-18s %-38s %s\n", cmd->name, cmd->params, cmd->desc);
+ }
+
+ printf("\n"
+ "Note:\n"
+ " Use @ owner to denote the zone name.\n"
+ " Empty or '--' <zone> parameter means all zones or all zones with a transaction.\n"
+ " Type <item> parameter in the form of <section>[<identifier>].<name>.\n"
+ " (*) indicates a local operation which requires a configuration.\n"
+ " (#) indicates an optionally blocking operation.\n"
+ " The '-b' and '-f' options can be placed right after the command name.\n");
+}
diff --git a/src/utils/knotc/commands.h b/src/utils/knotc/commands.h
new file mode 100644
index 0000000..22c3035
--- /dev/null
+++ b/src/utils/knotc/commands.h
@@ -0,0 +1,74 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "libknot/control/control.h"
+#include "knot/ctl/commands.h"
+
+/*! \brief Command condition flags. */
+typedef enum {
+ CMD_FNONE = 0, /*!< Empty flag. */
+ CMD_FREAD = 1 << 0, /*!< Required read access to config or confdb. */
+ CMD_FWRITE = 1 << 1, /*!< Required write access to confdb. */
+ CMD_FOPT_ITEM = 1 << 2, /*!< Optional item argument. */
+ CMD_FREQ_ITEM = 1 << 3, /*!< Required item argument. */
+ CMD_FOPT_DATA = 1 << 4, /*!< Optional item data argument. */
+ CMD_FOPT_ZONE = 1 << 5, /*!< Optional zone name argument. */
+ CMD_FREQ_ZONE = 1 << 6, /*!< Required zone name argument. */
+ CMD_FREQ_TXN = 1 << 7, /*!< Required open confdb transaction. */
+ CMD_FOPT_MOD = 1 << 8, /*!< Optional configured modules dependency. */
+ CMD_FREQ_MOD = 1 << 9, /*!< Required configured modules dependency. */
+ CMD_FLIST_SCHEMA = 1 << 10, /*!< List schema or possible option values. */
+} cmd_flag_t;
+
+struct cmd_desc;
+typedef struct cmd_desc cmd_desc_t;
+
+/*! \brief Command callback arguments. */
+typedef struct {
+ const cmd_desc_t *desc;
+ knot_ctl_t *ctl;
+ int argc;
+ const char **argv;
+ char flags[4];
+ bool force;
+ bool extended;
+ bool color;
+ bool color_force;
+ bool blocking;
+} cmd_args_t;
+
+/*! \brief Command callback description. */
+struct cmd_desc {
+ const char *name;
+ int (*fcn)(cmd_args_t *);
+ ctl_cmd_t cmd;
+ cmd_flag_t flags;
+};
+
+/*! \brief Command description. */
+typedef struct {
+ const char *name;
+ const char *params;
+ const char *desc;
+} cmd_help_t;
+
+/*! \brief Table of commands. */
+extern const cmd_desc_t cmd_table[];
+
+/*! \brief Prints commands help. */
+void print_commands(void);
diff --git a/src/utils/knotc/interactive.c b/src/utils/knotc/interactive.c
new file mode 100644
index 0000000..a03b8d3
--- /dev/null
+++ b/src/utils/knotc/interactive.c
@@ -0,0 +1,450 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <histedit.h>
+
+#include "knot/common/log.h"
+#include "utils/common/lookup.h"
+#include "utils/knotc/interactive.h"
+#include "utils/knotc/commands.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/string.h"
+
+#define PROGRAM_NAME "knotc"
+#define HISTORY_FILE ".knotc_history"
+
+extern params_t params;
+
+typedef struct {
+ const char **args;
+ int count;
+ bool dname;
+} dup_check_ctx_t;
+
+static void cmds_lookup(EditLine *el, const char *str, size_t str_len)
+{
+ lookup_t lookup;
+ int ret = lookup_init(&lookup);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+
+ // Fill the lookup with command names.
+ for (const cmd_desc_t *desc = cmd_table; desc->name != NULL; desc++) {
+ ret = lookup_insert(&lookup, desc->name, NULL);
+ if (ret != KNOT_EOK) {
+ goto cmds_lookup_finish;
+ }
+ }
+
+ (void)lookup_complete(&lookup, str, str_len, el, true);
+
+cmds_lookup_finish:
+ lookup_deinit(&lookup);
+}
+
+static void remove_duplicates(lookup_t *lookup, dup_check_ctx_t *check_ctx)
+{
+ if (check_ctx == NULL) {
+ return;
+ }
+
+ knot_dname_txt_storage_t dname = "";
+ for (int i = 0; i < check_ctx->count; i++) {
+ const char *arg = (check_ctx->args)[i];
+ size_t len = strlen(arg);
+ if (check_ctx->dname && len > 1 && arg[len - 1] != '.') {
+ strlcat(dname, arg, sizeof(dname));
+ strlcat(dname, ".", sizeof(dname));
+ arg = dname;
+ }
+ (void)lookup_remove(lookup, arg);
+ }
+}
+
+static void local_zones_lookup(EditLine *el, const char *str, size_t str_len,
+ dup_check_ctx_t *check_ctx)
+{
+ lookup_t lookup;
+ int ret = lookup_init(&lookup);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+
+ knot_dname_txt_storage_t buff;
+
+ // Fill the lookup with local zone names.
+ for (conf_iter_t iter = conf_iter(conf(), C_ZONE);
+ iter.code == KNOT_EOK; conf_iter_next(conf(), &iter)) {
+ conf_val_t val = conf_iter_id(conf(), &iter);
+ char *name = knot_dname_to_str(buff, conf_dname(&val), sizeof(buff));
+
+ ret = lookup_insert(&lookup, name, NULL);
+ if (ret != KNOT_EOK) {
+ conf_iter_finish(conf(), &iter);
+ goto local_zones_lookup_finish;
+ }
+ }
+
+ remove_duplicates(&lookup, check_ctx);
+ (void)lookup_complete(&lookup, str, str_len, el, true);
+
+local_zones_lookup_finish:
+ lookup_deinit(&lookup);
+}
+
+static void list_separators(EditLine *el, const char *separators)
+{
+ lookup_t lookup;
+ if (lookup_init(&lookup) != KNOT_EOK) {
+ return;
+ }
+
+ size_t count = strlen(separators);
+ for (int i = 0; i < count; i++) {
+ char sep[2] = { separators[i] };
+ (void)lookup_insert(&lookup, sep, NULL);
+ }
+ (void)lookup_complete(&lookup, "", 0, el, false);
+
+ lookup_deinit(&lookup);
+}
+
+static bool rmt_lookup(EditLine *el, const char *str, size_t str_len,
+ const char *section, const char *item, const char *id,
+ dup_check_ctx_t *check_ctx, bool add_space, const char *flags,
+ knot_ctl_idx_t idx)
+{
+ const cmd_desc_t *desc = cmd_table;
+ while (desc->name != NULL && desc->cmd != CTL_CONF_LIST) {
+ desc++;
+ }
+ assert(desc->name != NULL);
+
+ knot_ctl_data_t query = {
+ [KNOT_CTL_IDX_CMD] = ctl_cmd_to_str(desc->cmd),
+ [KNOT_CTL_IDX_SECTION] = section,
+ [KNOT_CTL_IDX_ITEM] = item,
+ [KNOT_CTL_IDX_ID] = id,
+ [KNOT_CTL_IDX_FLAGS] = flags
+ };
+
+ lookup_t lookup;
+ knot_ctl_t *ctl = NULL;
+ bool found = false;
+
+ if (set_ctl(&ctl, params.socket, DEFAULT_CTL_TIMEOUT_MS, desc) != KNOT_EOK ||
+ knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &query) != KNOT_EOK ||
+ knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL) != KNOT_EOK ||
+ lookup_init(&lookup) != KNOT_EOK) {
+ unset_ctl(ctl);
+ return found;
+ }
+
+ while (true) {
+ knot_ctl_type_t type;
+ knot_ctl_data_t reply;
+
+ if (knot_ctl_receive(ctl, &type, &reply) != KNOT_EOK) {
+ goto rmt_lookup_finish;
+ }
+
+ if (type != KNOT_CTL_TYPE_DATA && type != KNOT_CTL_TYPE_EXTRA) {
+ break;
+ }
+
+ const char *error = reply[KNOT_CTL_IDX_ERROR];
+ if (error != NULL) {
+ printf("\nnotice: (%s)\n", error);
+ goto rmt_lookup_finish;
+ }
+
+ // Insert the received name into the lookup.
+ if (lookup_insert(&lookup, reply[idx], NULL) != KNOT_EOK) {
+ goto rmt_lookup_finish;
+ }
+ }
+
+ remove_duplicates(&lookup, check_ctx);
+ if (lookup_complete(&lookup, str, str_len, el, add_space) == KNOT_EOK &&
+ str != NULL && strcmp(lookup.found.key, str) == 0) {
+ found = true;
+ }
+
+rmt_lookup_finish:
+ lookup_deinit(&lookup);
+ unset_ctl(ctl);
+
+ return found;
+}
+
+static bool id_lookup(EditLine *el, const char *str, size_t str_len,
+ const char *section, const cmd_desc_t *cmd_desc,
+ dup_check_ctx_t *ctx, bool add_space, bool zones)
+{
+ char flags[4] = "";
+ if (zones) {
+ strlcat(flags, CTL_FLAG_LIST_ZONES, sizeof(flags));
+ } else if (cmd_desc->flags & CMD_FREQ_TXN) {
+ strlcat(flags, CTL_FLAG_LIST_TXN, sizeof(flags));
+ }
+
+ return rmt_lookup(el, str, str_len, section, NULL, NULL, ctx, add_space,
+ flags, KNOT_CTL_IDX_ID);
+}
+
+static void val_lookup(EditLine *el, const char *str, size_t str_len,
+ const char *section, const char *item, const char *id,
+ dup_check_ctx_t *ctx, bool list_schema)
+{
+ char flags[4] = CTL_FLAG_LIST_TXN;
+ if (list_schema) {
+ strlcat(flags, CTL_FLAG_LIST_SCHEMA, sizeof(flags));
+ }
+
+ (void)rmt_lookup(el, str, str_len, section, item, id, ctx, true,
+ flags, KNOT_CTL_IDX_DATA);
+}
+
+static bool list_lookup(EditLine *el, const char *str, const char *section)
+{
+ const char *flags = CTL_FLAG_LIST_SCHEMA;
+ knot_ctl_idx_t idx = (section == NULL) ? KNOT_CTL_IDX_SECTION : KNOT_CTL_IDX_ITEM;
+
+ return rmt_lookup(el, str, strlen(str), section, NULL, NULL, NULL,
+ section != NULL, flags, idx);
+}
+
+static void item_lookup(EditLine *el, const char *str, const cmd_desc_t *cmd_desc)
+{
+ // List all sections.
+ if (str == NULL) {
+ (void)list_lookup(el, "", NULL);
+ return;
+ }
+
+ // Check for id specification.
+ char *id = (strchr(str, '['));
+ if (id != NULL) {
+ char *section = strndup(str, id - str);
+
+ // Check for completed id specification.
+ char *id_stop = (strchr(id, ']'));
+ if (id_stop != NULL) {
+ // Complete the item name.
+ if (*(id_stop + 1) == '.') {
+ (void)list_lookup(el, id_stop + 2, section);
+ } else {
+ list_separators(el, ".");
+ }
+ } else {
+ // Complete the section id.
+ if (id_lookup(el, id + 1, strlen(id + 1), section, cmd_desc,
+ NULL, false, false)) {
+ list_separators(el, "]");
+ }
+ }
+
+ free(section);
+ } else {
+ // Check for item specification.
+ char *dot = (strchr(str, '.'));
+ if (dot != NULL) {
+ // Complete the item name.
+ char *section = strndup(str, dot - str);
+ (void)list_lookup(el, dot + 1, section);
+ free(section);
+ } else {
+ // Complete the section name.
+ if (list_lookup(el, str, NULL)) {
+ list_separators(el, "[.");
+ }
+ }
+ }
+}
+
+static unsigned char complete(EditLine *el, int ch)
+{
+ int argc, token, pos;
+ const char **argv;
+
+ const LineInfo *li = el_line(el);
+ Tokenizer *tok = tok_init(NULL);
+
+ // Parse the line.
+ int ret = tok_line(tok, li, &argc, &argv, &token, &pos);
+ if (ret != 0) {
+ goto complete_exit;
+ }
+
+ // Show possible commands.
+ if (argc == 0) {
+ print_commands();
+ goto complete_exit;
+ }
+
+ // Complete the command name.
+ if (token == 0) {
+ cmds_lookup(el, argv[0], pos);
+ goto complete_exit;
+ }
+
+ // Find the command descriptor.
+ const cmd_desc_t *desc = cmd_table;
+ while (desc->name != NULL && strcmp(desc->name, argv[0]) != 0) {
+ desc++;
+ }
+ if (desc->name == NULL) {
+ goto complete_exit;
+ }
+
+ // Finish if a command with no or unsupported arguments.
+ if (desc->flags == CMD_FNONE || desc->flags == CMD_FREAD ||
+ desc->flags == CMD_FWRITE) {
+ goto complete_exit;
+ }
+
+ ret = set_config(desc, &params);
+ if (ret != KNOT_EOK) {
+ goto complete_exit;
+ }
+
+ // Complete the zone name.
+ if (desc->flags & (CMD_FREQ_ZONE | CMD_FOPT_ZONE)) {
+ if (token > 1 && !(desc->flags & CMD_FOPT_ZONE)) {
+ goto complete_exit;
+ }
+
+ dup_check_ctx_t ctx = { &argv[1], token - 1, true };
+ if (desc->flags & CMD_FREAD) {
+ local_zones_lookup(el, argv[token], pos, &ctx);
+ } else {
+ id_lookup(el, argv[token], pos, "zone", desc, &ctx, true, true);
+ }
+ goto complete_exit;
+ // Complete the section/id/item name or item value.
+ } else if (desc->flags & (CMD_FOPT_ITEM | CMD_FREQ_ITEM)) {
+ if (token == 1) {
+ item_lookup(el, argv[1], desc);
+ } else if (desc->flags & CMD_FOPT_DATA) {
+ char section[YP_MAX_TXT_KEY_LEN + 1] = "";
+ char item[YP_MAX_TXT_KEY_LEN + 1] = "";
+ char id[KNOT_DNAME_TXT_MAXLEN + 1] = "";
+
+ assert(YP_MAX_TXT_KEY_LEN == 127);
+ assert(KNOT_DNAME_TXT_MAXLEN == 1004);
+ if (sscanf(argv[1], "%127[^[][%1004[^]]].%127s", section, id, item) == 3 ||
+ sscanf(argv[1], "%127[^.].%127s", section, item) == 2) {
+ dup_check_ctx_t ctx = { &argv[2], token - 2 };
+ val_lookup(el, argv[token], pos, section, item, id,
+ &ctx, desc->flags & CMD_FLIST_SCHEMA);
+ }
+ }
+ goto complete_exit;
+ }
+
+complete_exit:
+ conf_update(NULL, CONF_UPD_FNONE);
+ tok_reset(tok);
+ tok_end(tok);
+
+ return CC_REDISPLAY;
+}
+
+static char *prompt(EditLine *el)
+{
+ return PROGRAM_NAME"> ";
+}
+
+int interactive_loop(params_t *process_params)
+{
+ char *hist_file = NULL;
+ const char *home = getenv("HOME");
+ if (home != NULL) {
+ hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE);
+ }
+ if (hist_file == NULL) {
+ log_notice("failed to get home directory");
+ }
+
+ EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr);
+ if (el == NULL) {
+ log_error("interactive mode not available");
+ free(hist_file);
+ return KNOT_ERROR;
+ }
+
+ History *hist = history_init();
+ if (hist == NULL) {
+ log_error("interactive mode not available");
+ el_end(el);
+ free(hist_file);
+ return KNOT_ERROR;
+ }
+
+ HistEvent hev = { 0 };
+ history(hist, &hev, H_SETSIZE, 1000);
+ history(hist, &hev, H_SETUNIQUE, 1);
+ el_set(el, EL_HIST, history, hist);
+ history(hist, &hev, H_LOAD, hist_file);
+
+ el_set(el, EL_TERMINAL, NULL);
+ el_set(el, EL_EDITOR, "emacs");
+ el_set(el, EL_PROMPT, prompt);
+ el_set(el, EL_SIGNAL, 1);
+ el_source(el, NULL);
+
+ // Warning: these two el_sets()'s always leak -- in libedit2 library!
+ // For more details see this commit's message.
+ el_set(el, EL_ADDFN, PROGRAM_NAME"-complete",
+ "Perform "PROGRAM_NAME" completion.", complete);
+ el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL);
+
+ int count;
+ const char *line;
+ while ((line = el_gets(el, &count)) != NULL && count > 0) {
+ Tokenizer *tok = tok_init(NULL);
+
+ // Tokenize the current line.
+ int argc;
+ const char **argv;
+ const LineInfo *li = el_line(el);
+ int ret = tok_line(tok, li, &argc, &argv, NULL, NULL);
+ if (ret == 0 && argc != 0) {
+ history(hist, &hev, H_ENTER, line);
+ history(hist, &hev, H_SAVE, hist_file);
+
+ // Process the command.
+ ret = process_cmd(argc, argv, process_params);
+ }
+
+ tok_reset(tok);
+ tok_end(tok);
+
+ // Check for the exit command.
+ if (ret == KNOT_CTL_ESTOP) {
+ break;
+ }
+ }
+
+ history_end(hist);
+ free(hist_file);
+
+ el_end(el);
+
+ return KNOT_EOK;
+}
diff --git a/src/utils/knotc/interactive.h b/src/utils/knotc/interactive.h
new file mode 100644
index 0000000..59690c7
--- /dev/null
+++ b/src/utils/knotc/interactive.h
@@ -0,0 +1,26 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/knotc/process.h"
+
+/*!
+ * Executes an interactive processing loop.
+ *
+ * \param[in] params Utility parameters.
+ */
+int interactive_loop(params_t *params);
diff --git a/src/utils/knotc/main.c b/src/utils/knotc/main.c
new file mode 100644
index 0000000..570469b
--- /dev/null
+++ b/src/utils/knotc/main.c
@@ -0,0 +1,172 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <getopt.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "contrib/strtonum.h"
+#include "knot/common/log.h"
+#include "utils/common/params.h"
+#include "utils/knotc/commands.h"
+#include "utils/knotc/interactive.h"
+#include "utils/knotc/process.h"
+
+#define PROGRAM_NAME "knotc"
+#define SPACE " "
+
+static void print_help(void)
+{
+ printf("Usage: %s [parameters] <action> [action_args]\n"
+ "\n"
+ "Parameters:\n"
+ " -c, --config <file> "SPACE"Use a textual configuration file.\n"
+ " "SPACE" (default %s)\n"
+ " -C, --confdb <dir> "SPACE"Use a binary configuration database directory.\n"
+ " "SPACE" (default %s)\n"
+ " -m, --max-conf-size <MiB>"SPACE"Set maximum size of the configuration database (max 10000 MiB).\n"
+ " "SPACE" (default %d MiB)\n"
+ " -s, --socket <path> "SPACE"Use a control UNIX socket path.\n"
+ " "SPACE" (default %s)\n"
+ " -t, --timeout <sec> "SPACE"Use a control socket timeout (max 86400 seconds).\n"
+ " "SPACE" (default %u seconds)\n"
+ " -b, --blocking "SPACE"Zone event trigger commands wait until the event is finished.\n"
+ " -e, --extended "SPACE"Show extended output.\n"
+ " -f, --force "SPACE"Forced operation. Overrides some checks.\n"
+ " -x, --mono "SPACE"Don't color the output.\n"
+ " -X, --color "SPACE"Force output colorization.\n"
+ " -v, --verbose "SPACE"Enable debug output.\n"
+ " -h, --help "SPACE"Print the program help.\n"
+ " -V, --version "SPACE"Print the program version.\n",
+ PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR,
+ CONF_MAPSIZE, RUN_DIR "/knot.sock", DEFAULT_CTL_TIMEOUT_MS / 1000);
+
+ print_commands();
+}
+
+params_t params = {
+ .max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024,
+ .timeout = -1
+};
+
+int main(int argc, char **argv)
+{
+ /* Long options. */
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "max-conf-size", required_argument, NULL, 'm' },
+ { "socket", required_argument, NULL, 's' },
+ { "timeout", required_argument, NULL, 't' },
+ { "blocking", no_argument, NULL, 'b' },
+ { "extended", no_argument, NULL, 'e' },
+ { "force", no_argument, NULL, 'f' },
+ { "mono", no_argument, NULL, 'x' },
+ { "color", no_argument, NULL, 'X' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ /* Set the time zone. */
+ tzset();
+
+ params.color = isatty(STDOUT_FILENO);
+ params.color_force = false;
+
+ /* Parse command line arguments */
+ int opt = 0;
+ while ((opt = getopt_long(argc, argv, "+c:C:m:s:t:befxXvhV", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'c':
+ params.orig_config = optarg;
+ break;
+ case 'C':
+ params.orig_confdb = optarg;
+ break;
+ case 'm':
+ if (str_to_size(optarg, &params.max_conf_size, 1, 10000) != KNOT_EOK) {
+ print_help();
+ return EXIT_FAILURE;
+ }
+ /* Convert to bytes. */
+ params.max_conf_size *= 1024 * 1024;
+ break;
+ case 's':
+ params.socket = optarg;
+ break;
+ case 't':
+ if (str_to_int(optarg, &params.timeout, 0, 86400) != KNOT_EOK) {
+ print_help();
+ return EXIT_FAILURE;
+ }
+ /* Convert to milliseconds. */
+ params.timeout *= 1000;
+ break;
+ case 'b':
+ params.blocking = true;
+ break;
+ case 'e':
+ params.extended = true;
+ break;
+ case 'f':
+ params.force = true;
+ break;
+ case 'v':
+ params.verbose = true;
+ break;
+ case 'x':
+ params.color = false;
+ break;
+ case 'X':
+ params.color = true;
+ params.color_force = true;
+ break;
+ case 'h':
+ print_help();
+ return EXIT_SUCCESS;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ return EXIT_SUCCESS;
+ default:
+ print_help();
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Set up simplified logging just to stdout/stderr. */
+ log_init();
+ log_levels_set(LOG_TARGET_STDOUT, LOG_SOURCE_ANY,
+ LOG_MASK(LOG_INFO) | LOG_MASK(LOG_NOTICE));
+ log_levels_set(LOG_TARGET_STDERR, LOG_SOURCE_ANY, LOG_UPTO(LOG_WARNING));
+ log_levels_set(LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, 0);
+ log_flag_set(LOG_FLAG_NOTIMESTAMP | LOG_FLAG_NOINFO);
+ if (params.verbose) {
+ log_levels_add(LOG_TARGET_STDOUT, LOG_SOURCE_ANY, LOG_MASK(LOG_DEBUG));
+ }
+
+ int ret;
+ if (argc - optind < 1) {
+ ret = interactive_loop(&params);
+ } else {
+ ret = process_cmd(argc - optind, (const char **)argv + optind, &params);
+ }
+
+ log_close();
+
+ return (ret == KNOT_EOK) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/src/utils/knotc/process.c b/src/utils/knotc/process.c
new file mode 100644
index 0000000..0f06891
--- /dev/null
+++ b/src/utils/knotc/process.c
@@ -0,0 +1,291 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <sys/stat.h>
+
+#include "contrib/openbsd/strlcat.h"
+#include "knot/conf/conf.h"
+#include "knot/common/log.h"
+#include "utils/knotc/commands.h"
+#include "utils/knotc/process.h"
+
+static const cmd_desc_t *get_cmd_desc(const char *command)
+{
+ /* Find requested command. */
+ const cmd_desc_t *desc = cmd_table;
+ while (desc->name != NULL) {
+ if (strcmp(desc->name, command) == 0) {
+ break;
+ }
+ desc++;
+ }
+ if (desc->name == NULL) {
+ log_error("invalid command '%s'", command);
+ return NULL;
+ }
+
+ return desc;
+}
+
+static bool get_cmd_force_flag(const char *arg)
+{
+ if (strcmp(arg, "-f") == 0 || strcmp(arg, "--force") == 0) {
+ return true;
+ }
+ return false;
+}
+
+static bool get_cmd_blocking_flag(const char *arg)
+{
+ if (strcmp(arg, "-b") == 0 || strcmp(arg, "--blocking") == 0) {
+ return true;
+ }
+ return false;
+}
+
+int set_config(const cmd_desc_t *desc, params_t *params)
+{
+ /* Reset the configuration paths (needed in the interactive mode). */
+ params->config = params->orig_config;
+ params->confdb = params->orig_confdb;
+
+ if (params->config != NULL && params->confdb != NULL) {
+ log_error("ambiguous configuration source");
+ return KNOT_EINVAL;
+ }
+
+ /* Mask relevant flags. */
+ cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE);
+ cmd_flag_t mod_flags = desc->flags & (CMD_FOPT_MOD | CMD_FREQ_MOD);
+
+ /* Choose the optimal config source. */
+ struct stat st;
+ bool import = false;
+ if (flags == CMD_FNONE && params->socket != NULL) {
+ /* Control operation, known socket, skip configuration. */
+ return KNOT_EOK;
+ } else if (params->confdb != NULL) {
+ import = false;
+ } else if (flags == CMD_FWRITE) {
+ import = false;
+ params->confdb = CONF_DEFAULT_DBDIR;
+ } else if (params->config != NULL){
+ import = true;
+ } else if (conf_db_exists(CONF_DEFAULT_DBDIR)) {
+ import = false;
+ params->confdb = CONF_DEFAULT_DBDIR;
+ } else if (stat(CONF_DEFAULT_FILE, &st) == 0) {
+ import = true;
+ params->config = CONF_DEFAULT_FILE;
+ } else if (flags != CMD_FNONE) {
+ log_error("no configuration source available");
+ return KNOT_EINVAL;
+ }
+
+ const char *src = import ? params->config : params->confdb;
+ log_debug("%s '%s'", import ? "config" : "confdb",
+ (src != NULL) ? src : "empty");
+
+ /* Prepare config flags. */
+ conf_flag_t conf_flags = CONF_FNOHOSTNAME;
+ if (params->confdb != NULL && !(flags & CMD_FWRITE)) {
+ conf_flags |= CONF_FREADONLY;
+ }
+ if (import || mod_flags & CMD_FOPT_MOD) {
+ conf_flags |= CONF_FOPTMODULES;
+ } else if (mod_flags & CMD_FREQ_MOD) {
+ conf_flags |= CONF_FREQMODULES;
+ }
+
+ /* Open confdb. */
+ conf_t *new_conf = NULL;
+ int ret = conf_new(&new_conf, conf_schema, params->confdb,
+ params->max_conf_size, conf_flags);
+ if (ret != KNOT_EOK) {
+ log_error("failed to open configuration database '%s' (%s)",
+ (params->confdb != NULL) ? params->confdb : "",
+ knot_strerror(ret));
+ return ret;
+ }
+
+ /* Import the config file. */
+ if (import) {
+ ret = conf_import(new_conf, params->config, true, false);
+ if (ret != KNOT_EOK) {
+ log_error("failed to load configuration file '%s' (%s)",
+ params->config, knot_strerror(ret));
+ conf_free(new_conf);
+ return ret;
+ }
+ }
+
+ /* Update to the new config. */
+ conf_update(new_conf, CONF_UPD_FNONE);
+
+ return KNOT_EOK;
+}
+
+int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc)
+{
+ if (desc == NULL) {
+ *ctl = NULL;
+ return KNOT_EINVAL;
+ }
+
+ /* Mask relevant flags. */
+ cmd_flag_t flags = desc->flags & (CMD_FREAD | CMD_FWRITE);
+
+ /* Check if control socket is required. */
+ if (flags != CMD_FNONE) {
+ *ctl = NULL;
+ return KNOT_EOK;
+ }
+
+ /* Get control socket path. */
+ char *path = NULL;
+ if (socket != NULL) {
+ path = strdup(socket);
+ } else {
+ conf_val_t listen_val = conf_get(conf(), C_CTL, C_LISTEN);
+ conf_val_t rundir_val = conf_get(conf(), C_SRV, C_RUNDIR);
+ char *rundir = conf_abs_path(&rundir_val, NULL);
+ path = conf_abs_path(&listen_val, rundir);
+ free(rundir);
+ }
+ if (path == NULL) {
+ log_error("empty control socket path");
+ return KNOT_EINVAL;
+ }
+
+ log_debug("socket '%s'", path);
+
+ *ctl = knot_ctl_alloc();
+ if (*ctl == NULL) {
+ free(path);
+ return KNOT_ENOMEM;
+ }
+
+ knot_ctl_set_timeout(*ctl, timeout);
+
+ int ret = knot_ctl_connect(*ctl, path);
+ if (ret != KNOT_EOK) {
+ knot_ctl_free(*ctl);
+ *ctl = NULL;
+ log_error("failed to connect to socket '%s' (%s)", path,
+ knot_strerror(ret));
+ free(path);
+ return ret;
+ }
+
+ free(path);
+
+ return KNOT_EOK;
+}
+
+void unset_ctl(knot_ctl_t *ctl)
+{
+ if (ctl == NULL) {
+ return;
+ }
+
+ int ret = knot_ctl_send(ctl, KNOT_CTL_TYPE_END, NULL);
+ if (ret != KNOT_EOK && ret != KNOT_ECONN) {
+ log_error("failed to finish control (%s)", knot_strerror(ret));
+ }
+
+ knot_ctl_close(ctl);
+ knot_ctl_free(ctl);
+}
+
+int process_cmd(int argc, const char **argv, params_t *params)
+{
+ if (argc == 0) {
+ return KNOT_ENOTSUP;
+ }
+
+ /* Check the command name. */
+ const cmd_desc_t *desc = get_cmd_desc(argv[0]);
+ if (desc == NULL) {
+ return KNOT_ENOENT;
+ }
+
+ /* Check for exit. */
+ if (desc->fcn == NULL) {
+ return KNOT_CTL_ESTOP;
+ }
+
+ /* Set up the configuration. */
+ int ret = set_config(desc, params);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Prepare command parameters. */
+ cmd_args_t args = {
+ .desc = desc,
+ .argc = argc - 1,
+ .argv = argv + 1,
+ .force = params->force,
+ .extended = params->extended,
+ .color = params->color,
+ .color_force = params->color_force,
+ .blocking = params->blocking
+ };
+
+ /* Check for special flags after command. */
+ while (args.argc > 0) {
+ if (get_cmd_force_flag(args.argv[0])) {
+ args.force = true;
+ args.argc--;
+ args.argv++;
+ } else if (get_cmd_blocking_flag(args.argv[0])) {
+ args.blocking = true;
+ args.argc--;
+ args.argv++;
+ } else {
+ break;
+ }
+ }
+
+ /* Prepare flags parameter. */
+ if (args.force) {
+ strlcat(args.flags, CTL_FLAG_FORCE, sizeof(args.flags));
+ }
+ if (args.blocking) {
+ strlcat(args.flags, CTL_FLAG_BLOCKING, sizeof(args.flags));
+ }
+
+ /* Set control interface if necessary. */
+ int cmd_timeout = params->timeout != -1 ? params->timeout : DEFAULT_CTL_TIMEOUT_MS;
+ if (args.blocking && params->timeout == -1) {
+ cmd_timeout = 0;
+ }
+ ret = set_ctl(&args.ctl, params->socket, cmd_timeout, desc);
+ if (ret != KNOT_EOK) {
+ conf_update(NULL, CONF_UPD_FNONE);
+ return ret;
+ }
+
+ /* Execute the command. */
+ ret = desc->fcn(&args);
+
+ /* Cleanup */
+ unset_ctl(args.ctl);
+ conf_update(NULL, CONF_UPD_FNONE);
+
+ return ret;
+}
diff --git a/src/utils/knotc/process.h b/src/utils/knotc/process.h
new file mode 100644
index 0000000..3946131
--- /dev/null
+++ b/src/utils/knotc/process.h
@@ -0,0 +1,78 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils/knotc/commands.h"
+
+#define DEFAULT_CTL_TIMEOUT_MS (60 * 1000)
+
+/*! Utility command line parameters. */
+typedef struct {
+ const char *orig_config;
+ const char *orig_confdb;
+ const char *config;
+ const char *confdb;
+ size_t max_conf_size;
+ const char *socket;
+ bool verbose;
+ bool extended;
+ bool force;
+ bool blocking;
+ int timeout;
+ bool color;
+ bool color_force;
+} params_t;
+
+/*!
+ * Prepares a proper configuration according to the specified command.
+ *
+ * \param[in] desc Utility command descriptor.
+ * \param[in] params Utility parameters.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int set_config(const cmd_desc_t *desc, params_t *params);
+
+/*!
+ * Establishes a control interface if necessary.
+ *
+ * \param[in] ctl Control context.
+ * \param[in] socket Control socket path.
+ * \param[in] timeout Control socket timeout.
+ * \param[in] desc Utility command descriptor.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int set_ctl(knot_ctl_t **ctl, const char *socket, int timeout, const cmd_desc_t *desc);
+
+/*!
+ * Cleans up the control context.
+ *
+ * \param[in] ctl Control context.
+ */
+void unset_ctl(knot_ctl_t *ctl);
+
+/*!
+ * Processes the given utility command.
+ *
+ * \param[in] argc Number of command arguments.
+ * \param[in] argv Command arguments.
+ * \param[in] params Utility parameters.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int process_cmd(int argc, const char **argv, params_t *params);