From 03d2b4a670b702da3ce51e45caddf3673c567351 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Tue, 10 Dec 2024 14:42:48 +0100 Subject: [PATCH] Introduction of cfgmgr Few points to note (and possibly discuss): - cfgmgr is build on top of LMDB, as it brings transaction (and so thread safe) support out of the box - LMDB keys are build in a way that we can support repeatable (see https://pad.isc.org/p/cfgmgr-proposal-v2 even if most of it is outdated, the section 2.0.2 about the way it's build is still relevant, otherwise I hope the code changes here are clear enough) - Each thread own its own cfgmgr context (which basically means LMDB transaction, and where the API points to inside the configuration) - In order to avoid allocations everytime we get/set a value (as well as few other operations) a single buffer is pre-allocated per-thread and per-transaction. Now, few internal helper functions directly use it (instead or, let's say, work on a parameter) and this might be confusing and error prone, I'm happy to change that if it is a worry. - The data type which can be read/wrote from cfgmgr is not exhaustive, more data type will be added (i.e. duration type, uint64 if needed, etc.) - This current implementation does not support inheritance (i.e. a non-specified view option won't use the option one). It's something we need to discuss, I see some options that could be put on top of that's here. - The "default" values, however, should be fine out-of-the-box: I think the default configuration in bin/named/config.c can be parse/"added" in cfgmgr first then user one on top of that (because writting an existing value override it). Obviously there would be no-way to "go back" to the default config only, but I don't see such use case in existing code. Even if we'd need such thing, that would be quite invasive and a full config reload would be advisable. (so we could add an API to entirely drop the cfgmgr data and start from scratch. But I don't see such use case right now anyway, so it's not there) - I hope the things a user of cfgmgr must know should be clearly explained in the cfgmgr.h file (if it's not the case, then I need to fix it -- That said I'll likely re-work the doc anyway). - I initially implemented a mechanism which would dynamically re-size key buffers in case keys are very long, but I remove this as LMDB doesn't supports key more than 511 bytes anyway. Instead I made assertions every time we build a key to make sure we don't exceed this value. --- lib/isccfg/Makefile.am | 6 +- lib/isccfg/cfgmgr.c | 717 ++++++++++++++++++++++++++++ lib/isccfg/include/isccfg/cfgmgr.h | 157 +++++++ tests/isccfg/Makefile.am | 3 +- tests/isccfg/cfgmgr_test.c | 721 +++++++++++++++++++++++++++++ 5 files changed, 1601 insertions(+), 3 deletions(-) create mode 100644 lib/isccfg/cfgmgr.c create mode 100644 lib/isccfg/include/isccfg/cfgmgr.h create mode 100644 tests/isccfg/cfgmgr_test.c diff --git a/lib/isccfg/Makefile.am b/lib/isccfg/Makefile.am index c5960400f8..1be429dfd3 100644 --- a/lib/isccfg/Makefile.am +++ b/lib/isccfg/Makefile.am @@ -10,7 +10,8 @@ libisccfg_la_HEADERS = \ include/isccfg/duration.h \ include/isccfg/grammar.h \ include/isccfg/kaspconf.h \ - include/isccfg/namedconf.h + include/isccfg/namedconf.h \ + include/isccfg/cfgmgr.h libisccfg_la_SOURCES = \ $(libisccfg_la_HEADERS) \ @@ -19,7 +20,8 @@ libisccfg_la_SOURCES = \ duration.c \ kaspconf.c \ namedconf.c \ - parser.c + parser.c \ + cfgmgr.c libisccfg_la_CPPFLAGS = \ $(AM_CPPFLAGS) \ diff --git a/lib/isccfg/cfgmgr.c b/lib/isccfg/cfgmgr.c new file mode 100644 index 0000000000..5369f50538 --- /dev/null +++ b/lib/isccfg/cfgmgr.c @@ -0,0 +1,717 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define DBPATH "/tmp/lmdb-exp" + +/* + * See MDB_MAXKEYSIZE documentation, but not accessible as defined in + * internal implementation. Having key with longer size won't work + * with LMDB. The value is 511 by default. + */ +#define BUFLEN 511 + +typedef struct openedclause openedclause_t; +struct openedclause { + char *name; + unsigned long id; + ISC_LINK(openedclause_t) link; +}; +typedef ISC_LIST(openedclause_t) openedclauses_t; + +typedef struct { + openedclauses_t openedclauses; + char *prefix; + char *buffer; + MDB_cursor *cursor; + MDB_txn *txn; + bool readonly; +} context_t; + +static isc_mem_t *mctx = NULL; +static MDB_env *env = NULL; +static thread_local context_t ctx = + (context_t){ .openedclauses = ISC_LIST_INITIALIZER, + .prefix = NULL, + .buffer = NULL, + .cursor = NULL, + .txn = NULL, + .readonly = false }; + +static unsigned long +parseid(const char *dbkey) { + unsigned long id = 0; + size_t idstarts; + size_t idends; + size_t keylen; + + REQUIRE(ctx.buffer != NULL); + REQUIRE(dbkey != NULL); + + /* + * starts checking after the prefix dot delimiter, i.e. if + * prefix is "foo" then the key will be "foo.1235..." so the + * start of the id (character 1) is at character index 4 + */ + idstarts = strlen(ctx.buffer); + idends = idstarts; + INSIST(idstarts > 0 && idends == idstarts); + INSIST(ctx.buffer[idstarts - 1] == '.'); + keylen = strlen(dbkey); + + /* + * Cutting the key form the dot after the identifier + */ + REQUIRE(keylen > idends); + while (dbkey[idends] != '.') { + idends++; + INSIST(keylen > idends); + } + + /* + * strtoul will stops as soon as it doesn't encounder a + * non-digit number, so no need to get an extra buffer, copy + * the dbkey and add a null byte after the last digit. + */ + id = strtoul(dbkey + idstarts, NULL, 10); + ENSURE(id > 0); + return id; +} + +isc_result_t +cfgmgr_init(void) { + int result = ISC_R_SUCCESS; + char dbname[BUFLEN]; + char dblockname[BUFLEN]; + uint32_t random; + + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses)); + REQUIRE(ctx.prefix == NULL); + REQUIRE(ctx.buffer == NULL); + REQUIRE(ctx.cursor == NULL); + REQUIRE(ctx.txn == NULL); + REQUIRE(mctx == NULL); + REQUIRE(env == NULL); + + isc_mem_create(&mctx); + INSIST(mctx != NULL); + + result = mdb_env_create(&env); + if (result != 0) { + result = ISC_R_FAILURE; + goto cleanup; + } + + /* + * Using MDB_NOSYNC as it avoid force disk flush after a + * transaction. It's quicker and in our case we don't need it + * as we delete the only link to the inode right away (so disk + * corruption doesn't matter: as soon as the process is dead, + * the disk data is dead as well) + */ + random = arc4random(); + REQUIRE(snprintf(dbname, BUFLEN, "%s-%u", DBPATH, random) < BUFLEN); + REQUIRE(snprintf(dblockname, BUFLEN, "%s-%u-lock", DBPATH, random) < + BUFLEN); + result = mdb_env_open(env, dbname, MDB_NOSYNC | MDB_NOSUBDIR, 0600); + if (result != 0) { + result = ISC_R_FAILURE; + goto cleanup; + } + + remove(dbname); + remove(dblockname); + + ENSURE(env != NULL); + goto out; + +cleanup: + if (env != NULL) { + mdb_env_close(env); + env = NULL; + } + +out: + return result; +} + +void +cfgmgr_deinit(void) { + /* + * Well, I'm on the fence about those context checks... It's + * good to have, but because they thread specific, it doesn't + * means there isn't a thread somewhere which haven't released + * its opened clauses and not the one calling cfgmgr_deinit, + * then we'll leak those - even if unlikely as this function + * should be call late in shutdown flow. (That said it's just + * an extra clue, because destroying the context will assert + * anyway, as some memory would not be released yet). + */ + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses)); + REQUIRE(ctx.prefix == NULL); + REQUIRE(ctx.buffer == NULL); + REQUIRE(ctx.cursor == NULL); + REQUIRE(ctx.txn == NULL); + REQUIRE(env != NULL); + REQUIRE(mctx != NULL); + mdb_env_close(env); + env = NULL; + isc_mem_destroy(&mctx); + ENSURE(mctx == NULL); +} + +static void +buildkey(const char *name, bool trailingdot) { + size_t written; + const char *prefix = ISC_LIST_EMPTY(ctx.openedclauses) ? "" + : ctx.prefix; + const char *dot = trailingdot ? "." : ""; + + REQUIRE(ctx.buffer != NULL); + written = snprintf(ctx.buffer, BUFLEN, "%s%s%s", prefix, name, dot); + INSIST(written <= BUFLEN); +} + +static isc_result_t +open_findclause(const char *name, unsigned long *id) { + isc_result_t result = ISC_R_SUCCESS; + MDB_val dbkey; + size_t dotpos = 0; + + REQUIRE(name != NULL); + REQUIRE(ctx.buffer != NULL); + REQUIRE(ctx.txn != NULL); + REQUIRE(ctx.cursor != NULL); + + buildkey(name, true); + dotpos = strlen(ctx.buffer) - 1; + dbkey = (MDB_val){ .mv_size = strlen(ctx.buffer) + 1, + .mv_data = (char *)ctx.buffer }; + + /* + * Let's use LMDB prefix search because the first clause + * key/val won't just have the "name.id" prefix, but also the + * id and the first property name (so "name.id.prop"). + */ + if (mdb_cursor_get(ctx.cursor, &dbkey, NULL, MDB_SET_RANGE) != 0) { + result = ISC_R_NOTFOUND; + goto out; + } + + /* + * LMDB found a key which starts by "prefix", so let's make + * sure it's actually the same prefix by checking the found + * key has an immediate leading dot + */ + if (dbkey.mv_size <= dotpos || ((char *)dbkey.mv_data)[dotpos] != '.') { + result = ISC_R_NOTFOUND; + goto out; + } + + /* + * We found the clause. Let's extract its ID + */ + *id = parseid(dbkey.mv_data); + +out: + return result; +} + +static void +updateprefix(void) { + size_t written = 0; + + REQUIRE(ctx.prefix != NULL); + + if (ISC_LIST_EMPTY(ctx.openedclauses)) { + return; + } + + for (openedclause_t *clause = ISC_LIST_TAIL(ctx.openedclauses); + clause != NULL; clause = ISC_LIST_PREV(clause, link)) + { + written += snprintf(ctx.prefix + written, BUFLEN - written, + "%s.%zu.", clause->name, clause->id); + INSIST(written <= BUFLEN); + } +} + +static void +pushclause(const char *name, unsigned long id) { + openedclause_t *clause = isc_mem_get(mctx, sizeof(*clause)); + + *clause = (openedclause_t){ + .name = isc_mem_allocate(mctx, strlen(name) + 1), .id = id + }; + strcpy(clause->name, name); + ENSURE(clause->id > 0); + ISC_LIST_PREPEND(ctx.openedclauses, clause, link); + updateprefix(); +} + +static void +freectx(void) { + REQUIRE(ctx.buffer != NULL && ctx.prefix != NULL); + + isc_mem_free(mctx, ctx.buffer); + ctx.buffer = NULL; + isc_mem_free(mctx, ctx.prefix); + ctx.prefix = NULL; + ctx.txn = NULL; + ctx.cursor = NULL; +} + +static isc_result_t +starttransaction(bool readonly) { + isc_result_t result = ISC_R_SUCCESS; + MDB_dbi dbi; + + REQUIRE(env != NULL); + REQUIRE(ctx.prefix == NULL); + REQUIRE(ctx.buffer == NULL); + REQUIRE(ctx.txn == NULL); + REQUIRE(ctx.cursor == NULL); + + if (mdb_txn_begin(env, NULL, readonly ? MDB_RDONLY : 0, &ctx.txn) != 0) + { + result = ISC_R_FAILURE; + goto cleanup; + } + INSIST(ctx.txn != NULL); + + if (mdb_dbi_open(ctx.txn, NULL, MDB_CREATE | MDB_DUPSORT, &dbi) != 0) { + result = ISC_R_FAILURE; + goto cleanup; + } + + if (mdb_cursor_open(ctx.txn, dbi, &ctx.cursor) != 0) { + result = ISC_R_FAILURE; + goto cleanup; + } + INSIST(ctx.cursor != NULL); + ctx.readonly = readonly; + ctx.buffer = isc_mem_allocate(mctx, BUFLEN); + ctx.prefix = isc_mem_allocate(mctx, BUFLEN); + goto out; + +cleanup: + if (ctx.txn) { + mdb_txn_abort(ctx.txn); + freectx(); + } + ENSURE(ctx.buffer == NULL && ctx.prefix == NULL); + +out: + return result; +} + +static isc_result_t +open_toplevel(const char *name, bool readonly) { + isc_result_t result = ISC_R_SUCCESS; + unsigned long id = 0; + + REQUIRE(env != NULL); + REQUIRE(name != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses)); + REQUIRE(ctx.prefix == NULL); + REQUIRE(ctx.buffer == NULL); + REQUIRE(ctx.txn == NULL); + REQUIRE(ctx.cursor == NULL); + + /* + * We're opening a clause at top-level, so let's start a + * transaction + */ + result = starttransaction(readonly); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + /* + * Now let's try to find the clause... + */ + result = open_findclause(name, &id); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + /* + * The clause is found, let's enqueue the clause in + * context. the clause is now opened + */ + pushclause(name, id); + goto out; + +cleanup: + if (ctx.txn) { + mdb_txn_abort(ctx.txn); + freectx(); + } + +out: + return result; +} + +static isc_result_t +open_nested(const char *name) { + isc_result_t result = ISC_R_SUCCESS; + unsigned long id = 0; + + REQUIRE(env != NULL); + REQUIRE(name != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses) == false); + REQUIRE(ctx.prefix != NULL); + REQUIRE(ctx.buffer != NULL); + REQUIRE(ctx.txn != NULL); + REQUIRE(ctx.cursor != NULL); + + result = open_findclause(name, &id); + if (result != ISC_R_SUCCESS) { + goto out; + } + + pushclause(name, id); + +out: + return result; +} + +isc_result_t +cfgmgr_openrw(const char *name) { + return open_toplevel(name, false); +} + +isc_result_t +cfgmgr_open(const char *name) { + return ISC_LIST_EMPTY(ctx.openedclauses) ? open_toplevel(name, true) + : open_nested(name); +} + +static void +popclause(void) { + REQUIRE(env != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses) == false); + + openedclause_t *clause = ISC_LIST_HEAD(ctx.openedclauses); + ISC_LIST_UNLINK(ctx.openedclauses, clause, link); + isc_mem_free(mctx, clause->name); + isc_mem_put(mctx, clause, sizeof(*clause)); + updateprefix(); +} + +isc_result_t +cfgmgr_close(void) { + isc_result_t result = ISC_R_SUCCESS; + + REQUIRE(env != NULL); + + if (ISC_LIST_EMPTY(ctx.openedclauses)) { + REQUIRE(ctx.prefix == NULL); + REQUIRE(ctx.buffer == NULL); + REQUIRE(ctx.txn == NULL); + REQUIRE(ctx.cursor == NULL); + result = ISC_R_NOTBOUND; + goto out; + } + + popclause(); + + if (ISC_LIST_EMPTY(ctx.openedclauses)) { + mdb_cursor_close(ctx.cursor); + if (mdb_txn_commit(ctx.txn) != 0) { + result = ISC_R_FAILURE; + } + freectx(); + } + +out: + return result; +} + +isc_result_t +cfgmgr_delclause(void) { + MDB_val dbkey; + + REQUIRE(env != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses) == false); + REQUIRE(ctx.prefix != NULL); + REQUIRE(ctx.buffer != NULL); + REQUIRE(ctx.txn != NULL); + REQUIRE(ctx.cursor != NULL); + REQUIRE(ctx.readonly == false); + + dbkey = (MDB_val){ .mv_size = strlen(ctx.prefix) + 1, + .mv_data = ctx.prefix }; + do { + /* + * even though the key is modified by mdb_cursor_get + * on each run (and is the exact current key) we're + * good: MDB_SET_RANGE of the current key will point + * to the next one with the same prefix as soon it + * gets deleted + */ + int mdbres = mdb_cursor_get(ctx.cursor, &dbkey, NULL, + MDB_SET_RANGE); + if (mdbres == MDB_NOTFOUND) { + break; + } + + if (strncmp(ctx.prefix, dbkey.mv_data, strlen(ctx.prefix)) != 0) + { + break; + } + + /* + * NDB_NODUPDATA not strictly needed here, but we + * avoid extra iterations if there are lists in the + * clause + */ + REQUIRE(mdb_cursor_del(ctx.cursor, MDB_NODUPDATA) == 0); + } while (1); + + return cfgmgr_close(); +} + +isc_result_t +cfgmgr_newclause(const char *name) { + isc_result_t result = ISC_R_SUCCESS; + + REQUIRE(name != NULL); + REQUIRE(env != NULL); + + if (ctx.txn == NULL || ctx.cursor == NULL) { + result = starttransaction(false); + } + + if (result == ISC_R_SUCCESS) { + INSIST(ctx.txn != NULL); + INSIST(ctx.buffer != NULL); + INSIST(ctx.cursor != NULL); + INSIST(ctx.readonly == false); + pushclause(name, arc4random()); + INSIST(ctx.prefix != NULL); + } + + return result; +} + +isc_result_t +cfgmgr_nextclause(void) { + isc_result_t result = ISC_R_SUCCESS; + MDB_val dbkey; + unsigned long id; + size_t idstarts = 0; + size_t written = 0; + + REQUIRE(env != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses) == false); + REQUIRE(ctx.prefix != NULL); + REQUIRE(ctx.buffer != NULL); + REQUIRE(ctx.txn != NULL); + REQUIRE(ctx.cursor != NULL); + + /* + * Let's pick the very next id (even if doesn't exists) of the + * current clause + */ + id = ISC_LIST_HEAD(ctx.openedclauses)->id + 1; + + /* + * Variant of updateprefix, but this time we put a incremented + * key for the currently opened clause. We also keep track of + * when the current clause id starts. + */ + for (openedclause_t *clause = ISC_LIST_TAIL(ctx.openedclauses); + clause != NULL; clause = ISC_LIST_PREV(clause, link)) + { + bool last = ISC_LIST_PREV(clause, link) == NULL; + + if (last) { + idstarts = written + strlen(clause->name) + 1; + } + written += snprintf(ctx.buffer + written, BUFLEN - written, + "%s.%zu.", clause->name, + last ? id : clause->id); + INSIST(written <= BUFLEN); + } + INSIST(idstarts > 0); + dbkey = (MDB_val){ .mv_size = strlen(ctx.buffer) + 1, + .mv_data = ctx.buffer }; + + /* + * Looking for similar prefix name but with a bigger + * id. Thanks for LMDB sort and MDB_SET_RANGE, we'll bump to + * the first key/val of the next clause of the same type + */ + if (mdb_cursor_get(ctx.cursor, &dbkey, NULL, MDB_SET_RANGE) != 0) { + result = ISC_R_NOMORE; + goto out; + } + + /* + * Let's check if next found clause is same name + */ + REQUIRE(idstarts < BUFLEN); + if (strncmp(ctx.buffer, dbkey.mv_data, idstarts) != 0) { + result = ISC_R_NOMORE; + goto out; + } + + /* + * Gets the actual id of the next clause (so let's get rid of + * the fake id part from the prefix). Instead of pop/push a + * new clause, let's simply replace the id and update the + * prefix. + */ + ctx.buffer[idstarts] = 0; + ISC_LIST_HEAD(ctx.openedclauses)->id = parseid(dbkey.mv_data); + updateprefix(); + +out: + return result; +} + +static isc_result_t +getval(const char *name, cfgmgr_val_t *value) { + isc_result_t result = ISC_R_SUCCESS; + MDB_val dbkey; + MDB_val dbval; + const int opt = name == NULL ? MDB_NEXT_DUP : MDB_SET; + + REQUIRE(env != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses) == false); + REQUIRE(ctx.prefix != NULL); + REQUIRE(ctx.buffer != NULL); + REQUIRE(ctx.txn != NULL); + REQUIRE(ctx.cursor != NULL); + REQUIRE(value != NULL); + + if (name != NULL) { + buildkey(name, false); + } + dbkey = (MDB_val){ .mv_size = name == NULL ? 0 : strlen(ctx.buffer) + 1, + .mv_data = name == NULL ? NULL : ctx.buffer }; + if (mdb_cursor_get(ctx.cursor, &dbkey, &dbval, opt) != 0) { + result = opt == MDB_NEXT_DUP ? ISC_R_NOMORE : ISC_R_NOTFOUND; + goto out; + } + + memcpy(value, dbval.mv_data, sizeof(*value)); + if (value->type == STRING) { + value->data.string = ((char *)dbval.mv_data) + + sizeof(value->type); + } + +out: + return result; +} + +isc_result_t +cfgmgr_getval(const char *name, cfgmgr_val_t *value) { + return getval(name, value); +} + +isc_result_t +cfgmgr_getnextlistval(cfgmgr_val_t *value) { + return getval(NULL, value); +} + +static isc_result_t +setval(const char *name, const cfgmgr_val_t *value, bool list) { + isc_result_t result = ISC_R_SUCCESS; + MDB_val dbkey; + MDB_val dbval; + + REQUIRE(env != NULL); + REQUIRE(ISC_LIST_EMPTY(ctx.openedclauses) == false); + REQUIRE(ctx.prefix != NULL); + REQUIRE(ctx.buffer != NULL); + REQUIRE(ctx.txn != NULL); + REQUIRE(ctx.cursor != NULL); + REQUIRE(ctx.readonly == false); + REQUIRE(name != NULL); + REQUIRE(value != NULL || (value == NULL && list == false)); + + buildkey(name, false); + dbkey = (MDB_val){ .mv_size = strlen(ctx.buffer) + 1, + .mv_data = ctx.buffer }; + if (value == NULL) { + if (mdb_cursor_get(ctx.cursor, &dbkey, NULL, MDB_SET) == + MDB_NOTFOUND) + { + result = ISC_R_NOTFOUND; + goto out; + } + + REQUIRE(mdb_cursor_del(ctx.cursor, MDB_NODUPDATA) == 0); + goto out; + } + + if (value->type == STRING) { + dbval.mv_size = sizeof(*value) + strlen(value->data.string) + 1; + dbval.mv_data = isc_mem_allocate(mctx, dbval.mv_size); + memcpy(dbval.mv_data, value, sizeof(value->type)); + strcpy(((char *)dbval.mv_data) + sizeof(value->type), + value->data.string); + } else { + dbval = (MDB_val){ .mv_size = sizeof(*value), + /* + * LMDB won't modify the mv_data buffer but + * its API is designed w/o the const buffer. + */ + .mv_data = (void *)value }; + } + + if (list == false) { + /* + * Can't use MDB_NOOVERWRITE as it would override the + * data if the key/value already exists. Making a + * value copy ahead just in case is likely more + * expensive than an extra lookup + */ + if (mdb_cursor_get(ctx.cursor, &dbkey, NULL, MDB_SET) != + MDB_NOTFOUND) + { + REQUIRE(mdb_cursor_del(ctx.cursor, 0) == 0); + } + } + + REQUIRE(mdb_cursor_put(ctx.cursor, &dbkey, &dbval, 0) == 0); + if (value->type == STRING) { + isc_mem_free(mctx, dbval.mv_data); + } + +out: + return result; +} + +isc_result_t +cfgmgr_setval(const char *name, const cfgmgr_val_t *value) { + return setval(name, value, false); +} + +isc_result_t +cfgmgr_setnextlistval(const char *name, const cfgmgr_val_t *value) { + return setval(name, value, true); +} diff --git a/lib/isccfg/include/isccfg/cfgmgr.h b/lib/isccfg/include/isccfg/cfgmgr.h new file mode 100644 index 0000000000..2f0219f93e --- /dev/null +++ b/lib/isccfg/include/isccfg/cfgmgr.h @@ -0,0 +1,157 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#pragma once + +#include +#include + +/* + * Supported data types for read/write operations from/to cfgmgr. + */ +typedef enum { STRING = 0, BOOL, NONE, SOCKADDR, UINT32 } cfgmgr_type_t; + +/* + * Generic value holding the actual value and type value for + * read/write from/to cfgmgr. + * + * cfgmgr_type_t::NONE doesn't have associated value, + */ +typedef struct { + cfgmgr_type_t type; + union { + const char *string; + bool boolean; + isc_sockaddr_t sockaddr; + uint32_t uint32; + } data; +} cfgmgr_val_t; + +/* + * Get the property "name" in the opened clause into the caller + * allocated "value" and returns ISC_R_SUCCESS. Returns ISC_R_NOTFOUND + * and "*value" is not mutated if "name" is not found. Changes being + * made by other threads aren't visible until the clause (and its + * parent, if nested) is closed. If "name" is a list property, get its + * head. + */ +isc_result_t +cfgmgr_getval(const char *name, cfgmgr_val_t *value); + +/* + * Write "value" into the property "name" in the opened clause and + * returns ISC_R_SUCCESS. If the property already exists, it is + * overridden and even if the type is different. If "value" is NULL + * and the property exists, it will be deleted (applies for list + * properties as well). Changes being made can be visible only by the + * current thread until the clause (and its parent, if nested) is + * closed. + */ +isc_result_t +cfgmgr_setval(const char *name, const cfgmgr_val_t *value); + +/* + * Same as cfgmgr_getval but applies for elements after the head of a + * list property. The head is read using cfgmgr_getval as any other + * value, then subsequents calls to cfgmgr_getnextlistval will get the + * next elements in the list. When the end of the list is reached, + * ISC_R_NOMORE is returned. Calls to cfgmgr_getnextlistval name has + * to be made in immediate sequence (without intermediate + * cfgmgr_{set,get}val calls) to retreive each list element. + */ +isc_result_t +cfgmgr_getnextlistval(cfgmgr_val_t *value); + +/* + * Same as cfgmgr_setval but applies for a list property. Writes by + * appending "*value" at the end of the list property "name" in the + * opened clause and returns ISC_R_SUCCESS. If "name" property wasn't + * existing before (or wasn't a list) it's overriden. It is not + * possible to delete individual list element, only the whole list can + * be removed using cfgmgr_setval. + */ +isc_result_t +cfgmgr_setnextlistval(const char *name, const cfgmgr_val_t *value); + +/* + * If the opened clause is a repeatable clause (i.e. view, acl, etc.), + * internally closes the opened clause and open the next clause of the + * same type and returns ISC_R_SUCCESS. When there is no next clause + * of the same type, ISC_R_NOMORE is returned. + */ +isc_result_t +cfgmgr_nextclause(void); + +/* + * If used at top-level, create and open as read-write a new clause + * "name". If used inside an opened parent clause, then the parent (or + * parent or the parent, recursively) clause must have been opened + * read-write (so using cfgmgr_openrw or cfgmgr_newclause). + * + * Returns ISC_R_SUCCESS. Note that in order to have the new clause + * actually written in cfgmgr, at least one property needs to be set + * to that clause. + */ +isc_result_t +cfgmgr_newclause(const char *name); + +/* + * Delete and close the opened clause. (And thus all its properties, + * including nested clauses). If the clause was nested, the currently + * opened clause is now the parent clause. Otherwise, no clause is + * opened. Returns ISC_R_SUCCESS. + */ +isc_result_t +cfgmgr_delclause(void); + +/* + * Close the currently opened clause and returns ISC_R_SUCCESS. If the + * closed clause was nested, the currently opened clause is now the + * parent clause. Otherwise, no close is opened. If no clause was + * opened when the function was called, ISC_R_NOTBOUND is + * returned. Closing the top-level clause will applies all + * modifications done inside the clause. If something is going wrong, + * ISC_R_FAILURE is returned, and all modification made are discarded. + */ +isc_result_t +cfgmgr_close(void); + +/* + * Open the top-level clause "name" for reading and writting and + * returns ISC_R_SUCCESS. If the clause "name" is not found, returns + * ISC_R_NOTFOUND. + * + * This call will block if another thread has already a clause opened + * for reading and writting. Use cfgmgr_openro for reading only. + */ +isc_result_t +cfgmgr_openrw(const char *name); + +/* + * Open the clause "name" and returns ISC_R_SUCCES or ISC_R_NOTFOUND + * is the clause is not found. Two possible cases: + * + * - if called at top-level, it open the top-level clause as read + * only. + * + * - if called form within an opened clause, it open it with the same + * access than the already opened clause. + */ +isc_result_t +cfgmgr_open(const char *name); + +isc_result_t +cfgmgr_init(void); + +void +cfgmgr_deinit(void); diff --git a/tests/isccfg/Makefile.am b/tests/isccfg/Makefile.am index 1a1887d917..b7920067f4 100644 --- a/tests/isccfg/Makefile.am +++ b/tests/isccfg/Makefile.am @@ -16,6 +16,7 @@ LDADD += \ check_PROGRAMS = \ duration_test \ parser_test \ - grammar_test + grammar_test \ + cfgmgr_test include $(top_srcdir)/Makefile.tests diff --git a/tests/isccfg/cfgmgr_test.c b/tests/isccfg/cfgmgr_test.c new file mode 100644 index 0000000000..e513a5dd2f --- /dev/null +++ b/tests/isccfg/cfgmgr_test.c @@ -0,0 +1,721 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#include +#include +#include +#include +#include + +#define UNIT_TESTING +#include + +#include + +#include + +#define SUCCESS(result) assert_int_equal(result, ISC_R_SUCCESS) +#define NOTFOUND(result) assert_int_equal(result, ISC_R_NOTFOUND) +#define NOTBOUND(result) assert_int_equal(result, ISC_R_NOTBOUND) + +ISC_RUN_TEST_IMPL(cfgmgr_rw) { + cfgmgr_val_t val1; + cfgmgr_val_t val2; + + SUCCESS(cfgmgr_init()); + + NOTFOUND(cfgmgr_open("foo")); + SUCCESS(cfgmgr_newclause("foo")); + + val1 = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 4058304 }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 4058304); + + val1 = (cfgmgr_val_t){ .type = NONE }; + SUCCESS(cfgmgr_setval("prop3", &val1)); + SUCCESS(cfgmgr_getval("prop3", &val2)); + assert_int_equal(val2.type, NONE); + + val1 = (cfgmgr_val_t){ .type = BOOL, .data.boolean = true }; + SUCCESS(cfgmgr_setval("prop2", &val1)); + SUCCESS(cfgmgr_getval("prop2", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + + val1 = (cfgmgr_val_t){ .type = BOOL, .data.boolean = false }; + SUCCESS(cfgmgr_setval("anotherprop", &val1)); + SUCCESS(cfgmgr_getval("anotherprop", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, false); + + /* + * Let's check and adding other properties didn't affect the + * ones added previously + */ + SUCCESS(cfgmgr_getval("prop3", &val2)); + assert_int_equal(val2.type, NONE); + + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 4058304); + + SUCCESS(cfgmgr_getval("prop2", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + + /* + * Non existent property - it doesn't not mutate val. + */ + NOTFOUND(cfgmgr_getval("prop4", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + + NOTFOUND(cfgmgr_getval("p", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_open("foo")); + + /* + * Everything still there when closing and re-opening + * (read-only) the clause + */ + SUCCESS(cfgmgr_getval("prop3", &val2)); + assert_int_equal(val2.type, NONE); + + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 4058304); + + SUCCESS(cfgmgr_getval("prop2", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + + SUCCESS(cfgmgr_close()); + + NOTBOUND(cfgmgr_close()); + + /* + * Adding other clause (intentionally with a different name, + * but a common prefix in the name - those are still different + * clauses + */ + SUCCESS(cfgmgr_newclause("foo1")); + val1 = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 1234 }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 1234); + + val1 = (cfgmgr_val_t){ .type = NONE }; + SUCCESS(cfgmgr_setval("somestuff", &val1)); + SUCCESS(cfgmgr_getval("somestuff", &val2)); + assert_int_equal(val2.type, NONE); + + /* + * Make sure we don't mixes clause properties + */ + NOTFOUND(cfgmgr_getval("prop2", &val2)); + NOTFOUND(cfgmgr_getval("prop3", &val2)); + SUCCESS(cfgmgr_close()); + + /* + * let's reopen rw this time + */ + SUCCESS(cfgmgr_openrw("foo")); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 4058304); + SUCCESS(cfgmgr_getval("prop2", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_openrw("foo1")); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 1234); + SUCCESS(cfgmgr_getval("somestuff", &val2)); + assert_int_equal(val2.type, NONE); + + /* + * because we used openrw, we can do that + */ + val1.type = UINT32; + val1.data.uint32 = 999; + SUCCESS(cfgmgr_setval("somestuff2", &val1)); + SUCCESS(cfgmgr_getval("somestuff2", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 999); + + SUCCESS(cfgmgr_close()); + + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_parseid) { + cfgmgr_val_t val; + + /* + * Excercise the fact that even if properties/clause names are + * number, this doesn't confuse the id parser (in particular, + * validates the usage of strtoul is correct). This also + * exercise the nested clause and repeatable clauses with such + * odd names + */ + SUCCESS(cfgmgr_init()); + + SUCCESS(cfgmgr_newclause("123")); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 666666 }; + SUCCESS(cfgmgr_setval("123123", &val)); + SUCCESS(cfgmgr_newclause("456")); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 777777 }; + SUCCESS(cfgmgr_setval("456456", &val)); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_newclause("456")); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 888888 }; + SUCCESS(cfgmgr_setval("456456", &val)); + SUCCESS(cfgmgr_close()); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 9999 }; + SUCCESS(cfgmgr_setval("456456", &val)); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_open("123")); + SUCCESS(cfgmgr_getval("123123", &val)); + assert_int_equal(val.type, UINT32); + assert_int_equal(val.data.uint32, 666666); + SUCCESS(cfgmgr_getval("456456", &val)); + assert_int_equal(val.type, UINT32); + assert_int_equal(val.data.uint32, 9999); + + bool found_777777 = false; + bool found_888888 = false; + + SUCCESS(cfgmgr_open("456")); + SUCCESS(cfgmgr_getval("456456", &val)); + assert_int_equal(val.type, UINT32); + if (val.data.uint32 == 777777) { + found_777777 = true; + } else if (val.data.uint32 == 888888) { + found_888888 = true; + } else { + REQUIRE(false); + } + + SUCCESS(cfgmgr_nextclause()); + SUCCESS(cfgmgr_getval("456456", &val)); + assert_int_equal(val.type, UINT32); + if (val.data.uint32 == 777777) { + found_777777 = true; + } else if (val.data.uint32 == 888888) { + found_888888 = true; + } else { + REQUIRE(false); + } + + assert_true(found_777777); + assert_true(found_888888); + + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_override) { + cfgmgr_val_t val1; + cfgmgr_val_t val2; + + SUCCESS(cfgmgr_init()); + + SUCCESS(cfgmgr_newclause("foo")); + + val1 = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 4058304 }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 4058304); + + val1 = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 666 }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, UINT32); + assert_int_equal(val2.data.uint32, 666); + + val1 = (cfgmgr_val_t){ .type = BOOL, .data.boolean = false }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, false); + + val1 = (cfgmgr_val_t){ .type = BOOL, .data.boolean = true }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, BOOL); + assert_int_equal(val2.data.boolean, true); + + SUCCESS(cfgmgr_close()); + + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_rw_string) { + cfgmgr_val_t val1; + cfgmgr_val_t val2; + + SUCCESS(cfgmgr_init()); + + SUCCESS(cfgmgr_newclause("foo")); + + val1 = (cfgmgr_val_t){ .type = STRING, .data.string = "hey there!" }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, STRING); + assert_string_equal(val2.data.string, "hey there!"); + + val1 = (cfgmgr_val_t){ + .type = STRING, + .data.string = "hey there! hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!" + }; + SUCCESS(cfgmgr_setval("prop1", &val1)); + SUCCESS(cfgmgr_getval("prop1", &val2)); + assert_int_equal(val2.type, STRING); + assert_string_equal( + val2.data.string, + "hey there! hey there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey there!hey there!hey " + "there!hey there!hey there!hey there!hey there!hey there!hey " + "there!"); + + val1 = (cfgmgr_val_t){ .type = STRING, + .data.string = "foobarbaz stuff" }; + SUCCESS(cfgmgr_setval("shorterstring", &val1)); + SUCCESS(cfgmgr_getval("shorterstring", &val2)); + assert_int_equal(val2.type, STRING); + assert_string_equal(val2.data.string, "foobarbaz stuff"); + + SUCCESS(cfgmgr_close()); + + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_list) { + cfgmgr_val_t val; + + SUCCESS(cfgmgr_init()); + SUCCESS(cfgmgr_newclause("foo")); + + val = (cfgmgr_val_t){ .type = STRING, .data.string = "lst1" }; + SUCCESS(cfgmgr_setnextlistval("proplist", &val)); + val.data.string = "lst2"; + SUCCESS(cfgmgr_setnextlistval("proplist", &val)); + val.data.string = "lst3"; + SUCCESS(cfgmgr_setnextlistval("proplist", &val)); + val.data.string = "lst4"; + SUCCESS(cfgmgr_setnextlistval("proplist", &val)); + + val.data.string = "otherpropval"; + SUCCESS(cfgmgr_setval("otherprop", &val)); + + val.data.string = "zzzval"; + SUCCESS(cfgmgr_setval("zzz", &val)); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_open("foo")); + + SUCCESS(cfgmgr_getval("proplist", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst1"); + + /* + * calling it again, we stick to the head + */ + SUCCESS(cfgmgr_getval("proplist", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst1"); + + /* + * now we're moving on... + */ + SUCCESS(cfgmgr_getnextlistval(&val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst2"); + + SUCCESS(cfgmgr_getnextlistval(&val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst3"); + + SUCCESS(cfgmgr_getnextlistval(&val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst4"); + + assert_int_equal(cfgmgr_getnextlistval(&val), ISC_R_NOMORE); + assert_int_equal(cfgmgr_getnextlistval(&val), ISC_R_NOMORE); + + /* + * and start from the begining again + */ + SUCCESS(cfgmgr_getval("proplist", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst1"); + + /* + * move on in the list but re-start again + */ + SUCCESS(cfgmgr_getnextlistval(&val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst2"); + SUCCESS(cfgmgr_getval("proplist", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "lst1"); + + /* + * calling after reading a non-list property + */ + SUCCESS(cfgmgr_getval("zzz", &val)); + assert_int_equal(cfgmgr_getnextlistval(&val), ISC_R_NOMORE); + + SUCCESS(cfgmgr_close()); + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_repeatable_clauses) { + cfgmgr_val_t val1; + cfgmgr_val_t val2; + + SUCCESS(cfgmgr_init()); + + SUCCESS(cfgmgr_newclause("view")); + SUCCESS(cfgmgr_setval("p1", &(cfgmgr_val_t){ .type = STRING, + .data.string = "view1 p1 " + "val" })); + SUCCESS(cfgmgr_setval( + "p2", &(cfgmgr_val_t){ .type = BOOL, .data.boolean = false })); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_newclause("view")); + SUCCESS(cfgmgr_setval("p1", &(cfgmgr_val_t){ .type = STRING, + .data.string = "view2 p2 " + "val" })); + SUCCESS(cfgmgr_setval( + "p2", &(cfgmgr_val_t){ .type = BOOL, .data.boolean = true })); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_open("view")); + SUCCESS(cfgmgr_getval("p2", &val2)); + assert_int_equal(val2.type, BOOL); + + if (val2.data.boolean) { + SUCCESS(cfgmgr_getval("p1", &val1)); + assert_int_equal(val1.type, STRING); + assert_string_equal(val1.data.string, "view2 p2 val"); + } else { + SUCCESS(cfgmgr_getval("p1", &val1)); + assert_int_equal(val1.type, STRING); + assert_string_equal(val1.data.string, "view1 p1 val"); + } + + SUCCESS(cfgmgr_nextclause()); + + SUCCESS(cfgmgr_getval("p2", &val2)); + assert_int_equal(val2.type, BOOL); + + if (val2.data.boolean) { + SUCCESS(cfgmgr_getval("p1", &val1)); + assert_int_equal(val1.type, STRING); + assert_string_equal(val1.data.string, "view2 p2 val"); + } else { + SUCCESS(cfgmgr_getval("p1", &val1)); + assert_int_equal(val1.type, STRING); + assert_string_equal(val1.data.string, "view1 p1 val"); + } + + assert_int_equal(cfgmgr_nextclause(), ISC_R_NOMORE); + SUCCESS(cfgmgr_close()); + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_nested_clauses) { + cfgmgr_val_t val; + + SUCCESS(cfgmgr_init()); + + /* + * Let's start by writting then reading + * foo { bar { baz { gee: none; }; }; }; + */ + SUCCESS(cfgmgr_newclause("foo")); + SUCCESS(cfgmgr_newclause("bar")); + SUCCESS(cfgmgr_newclause("baz")); + val.type = NONE; + SUCCESS(cfgmgr_setval("gee", &val)); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + NOTBOUND(cfgmgr_close()); + SUCCESS(cfgmgr_open("foo")); + SUCCESS(cfgmgr_open("bar")); + SUCCESS(cfgmgr_open("baz")); + val.type = UINT32; + SUCCESS(cfgmgr_getval("gee", &val)); + assert_int_equal(val.type, NONE); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + + /* + * then let's delete bar and add some properties in foo and + * another nested clause + */ + SUCCESS(cfgmgr_openrw("foo")); + SUCCESS(cfgmgr_open("bar")); + SUCCESS(cfgmgr_delclause()); + SUCCESS(cfgmgr_newclause("foonewsubclause")); + val = (cfgmgr_val_t){ .type = STRING, .data.string = "abc" }; + SUCCESS(cfgmgr_setval("propsubclause", &val)); + SUCCESS(cfgmgr_close()); + val = (cfgmgr_val_t){ .type = STRING, .data.string = "propfooval" }; + SUCCESS(cfgmgr_setval("propfoo", &val)); + SUCCESS(cfgmgr_close()); + + NOTBOUND(cfgmgr_close()); + + SUCCESS(cfgmgr_open("foo")); + SUCCESS(cfgmgr_getval("propfoo", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "propfooval"); + NOTFOUND(cfgmgr_open("bar")); + SUCCESS(cfgmgr_open("foonewsubclause")); + SUCCESS(cfgmgr_getval("propsubclause", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "abc"); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + + /* + * Let's mix nested and repeatable clauses + */ + SUCCESS(cfgmgr_openrw("foo")); + SUCCESS(cfgmgr_newclause("foonewsubclause")); + + bool abc_found = false; + bool def_found = false; + + val = (cfgmgr_val_t){ .type = STRING, .data.string = "def" }; + SUCCESS(cfgmgr_setval("propsubclause", &val)); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + NOTBOUND(cfgmgr_close()); + + val.data.string = NULL; + SUCCESS(cfgmgr_open("foo")); + SUCCESS(cfgmgr_open("foonewsubclause")); + SUCCESS(cfgmgr_getval("propsubclause", &val)); + assert_int_equal(val.type, STRING); + if (strncmp(val.data.string, "abc", 3) == 0) { + abc_found = true; + } else if (strncmp(val.data.string, "def", 3) == 0) { + def_found = true; + } else { + REQUIRE(false); + } + + SUCCESS(cfgmgr_nextclause()); + SUCCESS(cfgmgr_getval("propsubclause", &val)); + assert_int_equal(val.type, STRING); + if (strncmp(val.data.string, "abc", 3) == 0) { + abc_found = true; + } else if (strncmp(val.data.string, "def", 3) == 0) { + def_found = true; + } else { + REQUIRE(false); + } + + assert_true(abc_found); + assert_true(def_found); + SUCCESS(cfgmgr_close()); + SUCCESS(cfgmgr_close()); + + cfgmgr_deinit(); +} + +ISC_RUN_TEST_IMPL(cfgmgr_delete) { + cfgmgr_val_t val; + + SUCCESS(cfgmgr_init()); + + /* + * foo is not found because nothing has been written in there + */ + SUCCESS(cfgmgr_newclause("foo")); + SUCCESS(cfgmgr_close()); + NOTFOUND(cfgmgr_open("foo")); + + SUCCESS(cfgmgr_newclause("foo")); + val = (cfgmgr_val_t){ .type = NONE }; + SUCCESS(cfgmgr_setval("prop1", &val)); + val = (cfgmgr_val_t){ .type = STRING, .data.string = "prop2val" }; + SUCCESS(cfgmgr_setval("prop2", &val)); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_open("foo")); + SUCCESS(cfgmgr_getval("prop1", &val)); + SUCCESS(cfgmgr_getval("prop2", &val)); + SUCCESS(cfgmgr_close()); + + /* + * let's delete prop1 and add a list as prop3 + */ + SUCCESS(cfgmgr_openrw("foo")); + SUCCESS(cfgmgr_setval("prop1", NULL)); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 123 }; + SUCCESS(cfgmgr_setnextlistval("prop3", &val)); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 456 }; + SUCCESS(cfgmgr_setnextlistval("prop3", &val)); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_open("foo")); + NOTFOUND(cfgmgr_getval("prop1", &val)); + SUCCESS(cfgmgr_getval("prop2", &val)); + assert_int_equal(val.type, STRING); + assert_string_equal(val.data.string, "prop2val"); + SUCCESS(cfgmgr_getval("prop3", &val)); + assert_int_equal(val.type, UINT32); + assert_int_equal(val.data.uint32, 123); + SUCCESS(cfgmgr_getnextlistval(&val)); + assert_int_equal(val.type, UINT32); + assert_int_equal(val.data.uint32, 456); + SUCCESS(cfgmgr_close()); + + /* + * let's delete prop2 and prop3, the whole close disappears + */ + SUCCESS(cfgmgr_openrw("foo")); + SUCCESS(cfgmgr_setval("prop2", NULL)); + SUCCESS(cfgmgr_setval("prop3", NULL)); + SUCCESS(cfgmgr_close()); + NOTFOUND(cfgmgr_open("foo")); + + /* + * let's now delete a clause in one go (w/o explicitely + * deleting its properties. Another clause exists as well, it + * is not deleted. + */ + SUCCESS(cfgmgr_newclause("foo")); + val = (cfgmgr_val_t){ .type = NONE }; + SUCCESS(cfgmgr_setval("prop1", &val)); + val = (cfgmgr_val_t){ .type = STRING, .data.string = "prop2val" }; + SUCCESS(cfgmgr_setval("prop2", &val)); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 123 }; + SUCCESS(cfgmgr_setnextlistval("prop3", &val)); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 456 }; + SUCCESS(cfgmgr_setnextlistval("prop3", &val)); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_newclause("fooo")); + val = (cfgmgr_val_t){ .type = NONE }; + SUCCESS(cfgmgr_setval("prop1", &val)); + val = (cfgmgr_val_t){ .type = STRING, .data.string = "prop2val" }; + SUCCESS(cfgmgr_setval("prop2", &val)); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 123 }; + SUCCESS(cfgmgr_setnextlistval("prop3", &val)); + val = (cfgmgr_val_t){ .type = UINT32, .data.uint32 = 456 }; + SUCCESS(cfgmgr_setnextlistval("prop3", &val)); + SUCCESS(cfgmgr_close()); + + SUCCESS(cfgmgr_openrw("foo")); + SUCCESS(cfgmgr_getval("prop1", &val)); + SUCCESS(cfgmgr_getval("prop2", &val)); + SUCCESS(cfgmgr_getval("prop3", &val)); + SUCCESS(cfgmgr_delclause()); + NOTFOUND(cfgmgr_open("foo")); + + SUCCESS(cfgmgr_open("fooo")); + SUCCESS(cfgmgr_getval("prop1", &val)); + SUCCESS(cfgmgr_getval("prop2", &val)); + + SUCCESS(cfgmgr_getval("prop3", &val)); + assert_int_equal(val.type, UINT32); + assert_int_equal(val.data.uint32, 123); + SUCCESS(cfgmgr_getnextlistval(&val)); + assert_int_equal(val.type, UINT32); + assert_int_equal(val.data.uint32, 456); + assert_int_equal(cfgmgr_getnextlistval(&val), ISC_R_NOMORE); + SUCCESS(cfgmgr_close()); + + cfgmgr_deinit(); +} + +static void * +cfgmgr_threads_worker(void *arg) { + sem_t *sems = arg; + + /* + * This one open ro, so won't block + */ + cfgmgr_open("foo"); + sem_wait(&sems[0]); + sem_post(&sems[1]); + cfgmgr_close(); + + return NULL; +} + +ISC_RUN_TEST_IMPL(cfgmgr_threads) { + pthread_t thread; + sem_t sems[2]; + + REQUIRE(sem_init(&sems[0], 0, 0) == 0); + REQUIRE(sem_init(&sems[1], 0, 0) == 0); + SUCCESS(cfgmgr_init()); + SUCCESS(cfgmgr_newclause("foo")); + SUCCESS(cfgmgr_setval("p", &(cfgmgr_val_t){ .type = NONE })); + cfgmgr_close(); + + REQUIRE(pthread_create(&thread, 0, cfgmgr_threads_worker, &sems) == 0); + SUCCESS(cfgmgr_openrw("foo")); + REQUIRE(sem_post(&sems[0]) == 0); + REQUIRE(sem_wait(&sems[1]) == 0); + cfgmgr_close(); + REQUIRE(pthread_join(thread, NULL) == 0); + + cfgmgr_deinit(); +} + +ISC_TEST_LIST_START +ISC_TEST_ENTRY(cfgmgr_rw) +ISC_TEST_ENTRY(cfgmgr_override) +ISC_TEST_ENTRY(cfgmgr_rw_string) +ISC_TEST_ENTRY(cfgmgr_list) +ISC_TEST_ENTRY(cfgmgr_delete) +ISC_TEST_ENTRY(cfgmgr_repeatable_clauses) +ISC_TEST_ENTRY(cfgmgr_nested_clauses) +ISC_TEST_ENTRY(cfgmgr_threads) +ISC_TEST_ENTRY(cfgmgr_parseid) +ISC_TEST_LIST_END +ISC_TEST_MAIN