summaryrefslogtreecommitdiffstats
path: root/src/knot/zone/zonedb-load.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/zone/zonedb-load.c')
-rw-r--r--src/knot/zone/zonedb-load.c643
1 files changed, 643 insertions, 0 deletions
diff --git a/src/knot/zone/zonedb-load.c b/src/knot/zone/zonedb-load.c
new file mode 100644
index 0000000..58b17ab
--- /dev/null
+++ b/src/knot/zone/zonedb-load.c
@@ -0,0 +1,643 @@
+/* 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 <assert.h>
+#include <unistd.h>
+#include <urcu.h>
+
+#include "knot/catalog/generate.h"
+#include "knot/common/log.h"
+#include "knot/conf/module.h"
+#include "knot/events/replan.h"
+#include "knot/journal/journal_metadata.h"
+#include "knot/zone/digest.h"
+#include "knot/zone/timers.h"
+#include "knot/zone/zone-load.h"
+#include "knot/zone/zone.h"
+#include "knot/zone/zonedb-load.h"
+#include "knot/zone/zonedb.h"
+#include "knot/zone/zonefile.h"
+#include "libknot/libknot.h"
+
+static bool zone_file_updated(conf_t *conf, const zone_t *old_zone,
+ const knot_dname_t *zone_name)
+{
+ assert(conf);
+ assert(zone_name);
+
+ if (old_zone == NULL) {
+ return false;
+ }
+
+ char *zonefile = conf_zonefile(conf, zone_name);
+ struct timespec mtime;
+ int ret = zonefile_exists(zonefile, &mtime);
+ free(zonefile);
+
+ if (ret == KNOT_EOK) {
+ return !(old_zone->zonefile.exists &&
+ old_zone->zonefile.mtime.tv_sec == mtime.tv_sec &&
+ old_zone->zonefile.mtime.tv_nsec == mtime.tv_nsec);
+ } else {
+ return old_zone->zonefile.exists;
+ }
+}
+
+static void zone_get_catalog_group(conf_t *conf, zone_t *zone)
+{
+ conf_val_t val = conf_zone_get(conf, C_CATALOG_GROUP, zone->name);
+ if (val.code == KNOT_EOK) {
+ zone->catalog_group = conf_str(&val);
+ }
+}
+
+static zone_t *create_zone_from(const knot_dname_t *name, server_t *server)
+{
+ zone_t *zone = zone_new(name);
+ if (!zone) {
+ return NULL;
+ }
+
+ zone->server = server;
+
+ int result = zone_events_setup(zone, server->workers, &server->sched);
+ if (result != KNOT_EOK) {
+ zone_free(&zone);
+ return NULL;
+ }
+
+ return zone;
+}
+
+static void replan_events(conf_t *conf, zone_t *zone, zone_t *old_zone)
+{
+ bool conf_updated = (old_zone->change_type & CONF_IO_TRELOAD);
+
+ conf_val_t digest = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name);
+ if (zone->contents != NULL && !zone_contents_digest_exists(zone->contents, conf_opt(&digest), true)) {
+ conf_updated = true;
+ }
+
+ zone->events.ufrozen = old_zone->events.ufrozen;
+ if ((zone_file_updated(conf, old_zone, zone->name) || conf_updated) && !zone_expired(zone)) {
+ replan_load_updated(zone, old_zone);
+ } else {
+ zone->zonefile = old_zone->zonefile;
+ memcpy(&zone->notifailed, &old_zone->notifailed, sizeof(zone->notifailed));
+ memset(&old_zone->notifailed, 0, sizeof(zone->notifailed));
+ replan_load_current(conf, zone, old_zone);
+ }
+}
+
+static zone_t *create_zone_reload(conf_t *conf, const knot_dname_t *name,
+ server_t *server, zone_t *old_zone)
+{
+ zone_t *zone = create_zone_from(name, server);
+ if (!zone) {
+ return NULL;
+ }
+
+ zone->contents = old_zone->contents;
+ zone_set_flag(zone, zone_get_flag(old_zone, ~0, false));
+
+ zone->timers = old_zone->timers;
+ zone_timers_sanitize(conf, zone);
+
+ if (old_zone->control_update != NULL) {
+ log_zone_warning(old_zone->name, "control transaction aborted");
+ zone_control_clear(old_zone);
+ }
+
+ zone->cat_members = old_zone->cat_members;
+ old_zone->cat_members = NULL;
+
+ zone->catalog_gen = old_zone->catalog_gen;
+ old_zone->catalog_gen = NULL;
+
+ return zone;
+}
+
+static zone_t *create_zone_new(conf_t *conf, const knot_dname_t *name,
+ server_t *server)
+{
+ zone_t *zone = create_zone_from(name, server);
+ if (!zone) {
+ return NULL;
+ }
+
+ int ret = zone_timers_read(&server->timerdb, name, &zone->timers);
+ if (ret != KNOT_EOK && ret != KNOT_ENODB && ret != KNOT_ENOENT) {
+ log_zone_error(zone->name, "failed to load persistent timers (%s)",
+ knot_strerror(ret));
+ zone_free(&zone);
+ return NULL;
+ }
+
+ zone_timers_sanitize(conf, zone);
+
+ conf_val_t role_val = conf_zone_get(conf, C_CATALOG_ROLE, name);
+ unsigned role = conf_opt(&role_val);
+ if (role == CATALOG_ROLE_MEMBER) {
+ conf_val_t catz = conf_zone_get(conf, C_CATALOG_ZONE, name);
+ assert(catz.code == KNOT_EOK); // conf consistency checked in conf/tools.c
+ zone->catalog_gen = knot_dname_copy(conf_dname(&catz), NULL);
+ if (zone->timers.catalog_member == 0) {
+ zone->timers.catalog_member = time(NULL);
+ }
+ if (zone->catalog_gen == NULL) {
+ log_zone_error(zone->name, "failed to initialize catalog member zone (%s)",
+ knot_strerror(KNOT_ENOMEM));
+ zone_free(&zone);
+ return NULL;
+ }
+ } else if (role == CATALOG_ROLE_GENERATE) {
+ zone->cat_members = catalog_update_new();
+ if (zone->cat_members == NULL) {
+ log_zone_error(zone->name, "failed to initialize catalog zone (%s)",
+ knot_strerror(KNOT_ENOMEM));
+ zone_free(&zone);
+ return NULL;
+ }
+ zone_set_flag(zone, ZONE_IS_CATALOG);
+ } else if (role == CATALOG_ROLE_INTERPRET) {
+ ret = catalog_open(&server->catalog);
+ if (ret != KNOT_EOK) {
+ log_error("failed to open catalog database (%s)", knot_strerror(ret));
+ }
+ zone_set_flag(zone, ZONE_IS_CATALOG);
+ }
+
+ if (zone_expired(zone)) {
+ // expired => force bootstrap, no load attempt
+ log_zone_info(zone->name, "zone will be bootstrapped");
+ assert(zone_is_slave(conf, zone));
+ replan_load_bootstrap(conf, zone);
+ } else {
+ log_zone_info(zone->name, "zone will be loaded");
+ // if load fails, fallback to bootstrap
+ replan_load_new(zone, role == CATALOG_ROLE_GENERATE);
+ }
+
+ return zone;
+}
+
+/*!
+ * \brief Load or reload the zone.
+ *
+ * \param conf Configuration.
+ * \param server Server.
+ * \param old_zone Already loaded zone (can be NULL).
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static zone_t *create_zone(conf_t *conf, const knot_dname_t *name, server_t *server,
+ zone_t *old_zone)
+{
+ assert(conf);
+ assert(name);
+ assert(server);
+
+ zone_t *z;
+
+ if (old_zone) {
+ z = create_zone_reload(conf, name, server, old_zone);
+ } else {
+ z = create_zone_new(conf, name, server);
+ }
+
+ if (z != NULL) {
+ zone_get_catalog_group(conf, z);
+ }
+
+ return z;
+}
+
+static void mark_changed_zones(knot_zonedb_t *zonedb, trie_t *changed)
+{
+ if (changed == NULL) {
+ return;
+ }
+
+ trie_it_t *it = trie_it_begin(changed);
+ for (; !trie_it_finished(it); trie_it_next(it)) {
+ const knot_dname_t *name =
+ (const knot_dname_t *)trie_it_key(it, NULL);
+
+ zone_t *zone = knot_zonedb_find(zonedb, name);
+ if (zone != NULL) {
+ conf_io_type_t type = conf_io_trie_val(it);
+ assert(!(type & CONF_IO_TSET));
+ zone->change_type = type;
+ }
+ }
+ trie_it_free(it);
+}
+
+static void zone_purge(conf_t *conf, zone_t *zone)
+{
+ (void)selective_zone_purge(conf, zone, PURGE_ZONE_ALL);
+}
+
+static zone_contents_t *zone_expire(zone_t *zone)
+{
+ zone->timers.next_expire = time(NULL);
+ zone->timers.next_refresh = zone->timers.next_expire;
+ return zone_switch_contents(zone, NULL);
+}
+
+static bool check_open_catalog(catalog_t *cat)
+{
+ int ret = knot_lmdb_exists(&cat->db);
+ switch (ret) {
+ case KNOT_ENODB:
+ return false;
+ case KNOT_EOK:
+ ret = catalog_open(cat);
+ if (ret == KNOT_EOK) {
+ return true;
+ }
+ // FALLTHROUGH
+ default:
+ log_error("failed to open persistent zone catalog");
+ }
+ return false;
+}
+
+static zone_t *reuse_member_zone(zone_t *zone, server_t *server, conf_t *conf,
+ reload_t mode, list_t *expired_contents)
+{
+ if (!zone_get_flag(zone, ZONE_IS_CAT_MEMBER, false)) {
+ return NULL;
+ }
+
+ catalog_upd_val_t *upd = catalog_update_get(&server->catalog_upd, zone->name);
+ if (upd != NULL) {
+ switch (upd->type) {
+ case CAT_UPD_UNIQ:
+ zone_purge(conf, zone);
+ knot_sem_wait(&zone->cow_lock);
+ ptrlist_add(expired_contents, zone_expire(zone), NULL);
+ knot_sem_post(&zone->cow_lock);
+ // FALLTHROUGH
+ case CAT_UPD_PROP:
+ zone->change_type = CONF_IO_TRELOAD;
+ break; // reload the member zone
+ case CAT_UPD_INVALID:
+ case CAT_UPD_MINOR:
+ return zone; // reuse the member zone
+ case CAT_UPD_REM:
+ return NULL; // remove the member zone
+ case CAT_UPD_ADD: // cannot add existing member
+ default:
+ assert(0);
+ return NULL;
+ }
+ } else if (mode & (RELOAD_COMMIT | RELOAD_CATALOG)) {
+ return zone; // reuse the member zone
+ }
+
+ zone_t *newzone = create_zone(conf, zone->name, server, zone);
+ if (newzone == NULL) {
+ log_zone_error(zone->name, "zone cannot be created");
+ } else {
+ assert(zone_get_flag(newzone, ZONE_IS_CAT_MEMBER, false));
+ conf_activate_modules(conf, server, newzone->name, &newzone->query_modules,
+ &newzone->query_plan);
+ }
+
+ return newzone;
+}
+
+// cold start of knot: add unchanged member zone to zonedb
+static zone_t *reuse_cold_zone(const knot_dname_t *zname, server_t *server, conf_t *conf)
+{
+ catalog_upd_val_t *upd = catalog_update_get(&server->catalog_upd, zname);
+ if (upd != NULL && upd->type == CAT_UPD_REM) {
+ return NULL; // zone will be removed immediately
+ }
+
+ zone_t *zone = create_zone(conf, zname, server, NULL);
+ if (zone == NULL) {
+ log_zone_error(zname, "zone cannot be created");
+ } else {
+ zone_set_flag(zone, ZONE_IS_CAT_MEMBER);
+ conf_activate_modules(conf, server, zone->name, &zone->query_modules,
+ &zone->query_plan);
+ }
+ return zone;
+}
+
+typedef struct {
+ knot_zonedb_t *zonedb;
+ server_t *server;
+ conf_t *conf;
+} reuse_cold_zone_ctx_t;
+
+static int reuse_cold_zone_cb(const knot_dname_t *member, _unused_ const knot_dname_t *owner,
+ const knot_dname_t *catz, _unused_ const char *group,
+ void *ctx)
+{
+ reuse_cold_zone_ctx_t *rcz = ctx;
+
+ zone_t *catz_z = knot_zonedb_find(rcz->zonedb, catz);
+ if (catz_z == NULL || !(catz_z->flags & ZONE_IS_CATALOG)) {
+ log_zone_warning(member, "orphaned catalog member zone, ignoring");
+ return KNOT_EOK;
+ }
+
+ zone_t *zone = reuse_cold_zone(member, rcz->server, rcz->conf);
+ if (zone == NULL) {
+ return KNOT_ENOMEM;
+ }
+ return knot_zonedb_insert(rcz->zonedb, zone);
+}
+
+static zone_t *add_member_zone(catalog_upd_val_t *val, knot_zonedb_t *check,
+ server_t *server, conf_t *conf)
+{
+ if (val->type != CAT_UPD_ADD) {
+ return NULL;
+ }
+
+ if (knot_zonedb_find(check, val->member) != NULL) {
+ log_zone_error(val->member, "zone already configured, ignoring");
+ return NULL;
+ }
+
+ zone_t *zone = create_zone(conf, val->member, server, NULL);
+ if (zone == NULL) {
+ log_zone_error(val->member, "zone cannot be created");
+ } else {
+ zone_set_flag(zone, ZONE_IS_CAT_MEMBER);
+ conf_activate_modules(conf, server, zone->name, &zone->query_modules,
+ &zone->query_plan);
+ log_zone_info(val->member, "zone added from catalog");
+ }
+ return zone;
+}
+
+/*!
+ * \brief Create new zone database.
+ *
+ * Zones that should be retained are just added from the old database to the
+ * new. New zones are loaded.
+ *
+ * \param conf New server configuration.
+ * \param server Server instance.
+ * \param mode Reload mode.
+ * \param expired_contents Out: ptrlist of zone_contents_t to be deep freed after sync RCU.
+ *
+ * \return New zone database.
+ */
+static knot_zonedb_t *create_zonedb(conf_t *conf, server_t *server, reload_t mode,
+ list_t *expired_contents)
+{
+ assert(conf);
+ assert(server);
+
+ knot_zonedb_t *db_old = server->zone_db;
+ knot_zonedb_t *db_new = knot_zonedb_new();
+ if (!db_new) {
+ return NULL;
+ }
+
+ /* Mark changed zones during dynamic configuration. */
+ if (mode == RELOAD_COMMIT) {
+ mark_changed_zones(db_old, conf->io.zones);
+ }
+
+ /* Process regular zones from the configuration. */
+ for (conf_iter_t iter = conf_iter(conf, C_ZONE); iter.code == KNOT_EOK;
+ conf_iter_next(conf, &iter)) {
+ conf_val_t id = conf_iter_id(conf, &iter);
+ const knot_dname_t *name = conf_dname(&id);
+
+ zone_t *old_zone = knot_zonedb_find(db_old, name);
+ if (old_zone != NULL && (mode & (RELOAD_COMMIT | RELOAD_CATALOG))) {
+ /* Reuse unchanged zone. */
+ if (!(old_zone->change_type & CONF_IO_TRELOAD)) {
+ knot_zonedb_insert(db_new, old_zone);
+ continue;
+ }
+ }
+
+ zone_t *zone = create_zone(conf, name, server, old_zone);
+ if (zone == NULL) {
+ log_zone_error(name, "zone cannot be created");
+ continue;
+ }
+
+ conf_activate_modules(conf, server, zone->name, &zone->query_modules,
+ &zone->query_plan);
+
+ knot_zonedb_insert(db_new, zone);
+ }
+
+ /* Purge decataloged zones before catalog removals are commited. */
+ catalog_it_t *cat_it = catalog_it_begin(&server->catalog_upd);
+ while (!catalog_it_finished(cat_it)) {
+ catalog_upd_val_t *upd = catalog_it_val(cat_it);
+ if (upd->type == CAT_UPD_REM) {
+ zone_t *zone = knot_zonedb_find(db_old, upd->member);
+ if (zone != NULL) {
+ zone->change_type = CONF_IO_TUNSET;
+ zone_purge(conf, zone);
+ }
+ }
+ catalog_it_next(cat_it);
+ }
+ catalog_it_free(cat_it);
+
+ int ret = catalog_update_commit(&server->catalog_upd, &server->catalog);
+ if (ret != KNOT_EOK) {
+ log_error("catalog, failed to apply changes (%s)", knot_strerror(ret));
+ return db_new;
+ }
+
+ /* Process existing catalog member zones. */
+ if (db_old != NULL) {
+ knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old);
+ while (!knot_zonedb_iter_finished(it)) {
+ zone_t *newzone = reuse_member_zone(knot_zonedb_iter_val(it),
+ server, conf, mode,
+ expired_contents);
+ if (newzone != NULL) {
+ knot_zonedb_insert(db_new, newzone);
+ }
+ knot_zonedb_iter_next(it);
+ }
+ knot_zonedb_iter_free(it);
+ } else if (check_open_catalog(&server->catalog)) {
+ reuse_cold_zone_ctx_t rcz = { db_new, server, conf };
+ ret = catalog_apply(&server->catalog, NULL, reuse_cold_zone_cb, &rcz, false);
+ if (ret != KNOT_EOK) {
+ log_error("catalog, failed to load member zones (%s)", knot_strerror(ret));
+ }
+ }
+
+ /* Process new catalog member zones. */
+ catalog_it_t *it = catalog_it_begin(&server->catalog_upd);
+ while (!catalog_it_finished(it)) {
+ catalog_upd_val_t *val = catalog_it_val(it);
+ zone_t *zone = add_member_zone(val, db_new, server, conf);
+ if (zone != NULL) {
+ knot_zonedb_insert(db_new, zone);
+ }
+ catalog_it_next(it);
+ }
+ catalog_it_free(it);
+
+ return db_new;
+}
+
+/*!
+ * \brief Schedule deletion of old zones, and free the zone db structure.
+ *
+ * \note Zone content may be preserved in the new zone database, in this case
+ * new and old zone share the contents. Shared content is not freed.
+ *
+ * \param conf New server configuration.
+ * \param db_old Old zone database to remove.
+ * \param server Server context.
+ */
+static void remove_old_zonedb(conf_t *conf, knot_zonedb_t *db_old,
+ server_t *server, reload_t mode)
+{
+ catalog_commit_cleanup(&server->catalog);
+
+ knot_zonedb_t *db_new = server->zone_db;
+
+ if (db_old == NULL) {
+ goto catalog_only;
+ }
+
+ knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old);
+ while (!knot_zonedb_iter_finished(it)) {
+ zone_t *zone = knot_zonedb_iter_val(it);
+ if (mode & (RELOAD_FULL | RELOAD_ZONES)) {
+ /* Check if reloaded (reused contents). */
+ zone_t *new_zone = knot_zonedb_find(db_new, zone->name);
+ if (new_zone != NULL) {
+ replan_events(conf, new_zone, zone);
+ zone->contents = NULL;
+ }
+ /* Completely new zone. */
+ } else {
+ /* Check if reloaded (reused contents). */
+ if (zone->change_type & CONF_IO_TRELOAD) {
+ zone_t *new_zone = knot_zonedb_find(db_new, zone->name);
+ assert(new_zone);
+ replan_events(conf, new_zone, zone);
+ zone->contents = NULL;
+ zone_free(&zone);
+ /* Check if removed (drop also contents). */
+ } else if (zone->change_type & CONF_IO_TUNSET) {
+ zone_free(&zone);
+ }
+ /* Completely reused zone. */
+ }
+ knot_zonedb_iter_next(it);
+ }
+ knot_zonedb_iter_free(it);
+
+catalog_only:
+
+ /* Clear catalog changes. No need to use mutex as this is done from main
+ * thread while all zone events are paused. */
+ catalog_update_clear(&server->catalog_upd);
+
+ if (mode & (RELOAD_FULL | RELOAD_ZONES)) {
+ knot_zonedb_deep_free(&db_old, false);
+ } else {
+ knot_zonedb_free(&db_old);
+ }
+}
+
+void zonedb_reload(conf_t *conf, server_t *server, reload_t mode)
+{
+ if (conf == NULL || server == NULL) {
+ return;
+ }
+
+ if (mode == RELOAD_COMMIT) {
+ assert(conf->io.flags & CONF_IO_FACTIVE);
+ if (conf->io.flags & CONF_IO_FRLD_ZONES) {
+ mode = RELOAD_ZONES;
+ }
+ }
+
+ list_t contents_tofree;
+ init_list(&contents_tofree);
+
+ catalog_update_finalize(&server->catalog_upd, &server->catalog, conf);
+ size_t cat_upd_size = trie_weight(server->catalog_upd.upd);
+ if (cat_upd_size > 0) {
+ log_info("catalog, updating, %zu changes", cat_upd_size);
+ }
+
+ /* Insert all required zones to the new zone DB. */
+ knot_zonedb_t *db_new = create_zonedb(conf, server, mode, &contents_tofree);
+ if (db_new == NULL) {
+ log_error("failed to create new zone database");
+ return;
+ }
+
+ catalogs_generate(db_new, server->zone_db);
+
+ /* Switch the databases. */
+ knot_zonedb_t **db_current = &server->zone_db;
+ knot_zonedb_t *db_old = rcu_xchg_pointer(db_current, db_new);
+
+ /* Wait for readers to finish reading old zone database. */
+ synchronize_rcu();
+
+ ptrlist_free_custom(&contents_tofree, NULL, (ptrlist_free_cb)zone_contents_deep_free);
+
+ /* Remove old zone DB. */
+ remove_old_zonedb(conf, db_old, server, mode);
+}
+
+int zone_reload_modules(conf_t *conf, server_t *server, const knot_dname_t *zone_name)
+{
+ zone_t **zone = knot_zonedb_find_ptr(server->zone_db, zone_name);
+ if (zone == NULL) {
+ return KNOT_ENOENT;
+ }
+ assert(knot_dname_is_equal((*zone)->name, zone_name));
+
+ zone_events_freeze_blocking(*zone);
+ knot_sem_wait(&(*zone)->cow_lock);
+
+ zone_t *newzone = create_zone(conf, zone_name, server, *zone);
+ if (newzone == NULL) {
+ return KNOT_ENOMEM;
+ }
+ conf_activate_modules(conf, server, newzone->name, &newzone->query_modules,
+ &newzone->query_plan);
+
+ zone_t *oldzone = rcu_xchg_pointer(zone, newzone);
+ synchronize_rcu();
+
+ replan_events(conf, newzone, oldzone);
+
+ assert(newzone->contents == oldzone->contents);
+ oldzone->contents = NULL; // contents have been re-used by newzone
+
+ knot_sem_post(&oldzone->cow_lock);
+ zone_free(&oldzone);
+
+ return KNOT_EOK;
+}