// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) 2018 NetDEF, Inc. * Renato Westphal */ #include #include "libfrr.h" #include "log.h" #include "lib_errors.h" #include "command.h" #include "db.h" #include "northbound.h" #include "northbound_db.h" int nb_db_init(void) { #ifdef HAVE_CONFIG_ROLLBACKS /* * NOTE: the delete_tail SQL trigger is used to implement a ring buffer * where only the last N transactions are recorded in the configuration * log. */ if (db_execute( "BEGIN TRANSACTION;\n" " CREATE TABLE IF NOT EXISTS transactions(\n" " client CHAR(32) NOT NULL,\n" " date DATETIME DEFAULT CURRENT_TIMESTAMP,\n" " comment CHAR(80) ,\n" " configuration TEXT NOT NULL\n" " );\n" " CREATE TRIGGER IF NOT EXISTS delete_tail\n" " AFTER INSERT ON transactions\n" " FOR EACH ROW\n" " BEGIN\n" " DELETE\n" " FROM\n" " transactions\n" " WHERE\n" " rowid%%%u=NEW.rowid%%%u AND rowid!=NEW.rowid;\n" " END;\n" "COMMIT;", NB_DLFT_MAX_CONFIG_ROLLBACKS, NB_DLFT_MAX_CONFIG_ROLLBACKS) != 0) return NB_ERR; #endif /* HAVE_CONFIG_ROLLBACKS */ return NB_OK; } int nb_db_transaction_save(const struct nb_transaction *transaction, uint32_t *transaction_id) { #ifdef HAVE_CONFIG_ROLLBACKS struct sqlite3_stmt *ss; const char *client_name; char *config_str = NULL; int ret = NB_ERR; /* * Use a transaction to ensure consistency between the INSERT and SELECT * queries. */ if (db_execute("BEGIN TRANSACTION;") != 0) return NB_ERR; ss = db_prepare( "INSERT INTO transactions\n" " (client, comment, configuration)\n" "VALUES\n" " (?, ?, ?);"); if (!ss) goto exit; client_name = nb_client_name(transaction->context.client); /* * Always record configurations in the XML format, save the default * values too, as this covers the case where defaults may change. */ if (lyd_print_mem(&config_str, transaction->config->dnode, LYD_XML, LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL) != 0) goto exit; if (db_bindf(ss, "%s%s%s", client_name, strlen(client_name), transaction->comment, strlen(transaction->comment), config_str ? config_str : "", config_str ? strlen(config_str) : 0) != 0) goto exit; if (db_run(ss) != SQLITE_OK) goto exit; db_finalize(&ss); /* * transaction_id is an optional output parameter that provides the ID * of the recorded transaction. */ if (transaction_id) { ss = db_prepare("SELECT last_insert_rowid();"); if (!ss) goto exit; if (db_run(ss) != SQLITE_ROW) goto exit; if (db_loadf(ss, "%i", transaction_id) != 0) goto exit; db_finalize(&ss); } if (db_execute("COMMIT;") != 0) goto exit; ret = NB_OK; exit: if (config_str) free(config_str); if (ss) db_finalize(&ss); if (ret != NB_OK) (void)db_execute("ROLLBACK TRANSACTION;"); return ret; #else /* HAVE_CONFIG_ROLLBACKS */ return NB_OK; #endif /* HAVE_CONFIG_ROLLBACKS */ } struct nb_config *nb_db_transaction_load(uint32_t transaction_id) { struct nb_config *config = NULL; #ifdef HAVE_CONFIG_ROLLBACKS struct lyd_node *dnode; const char *config_str; struct sqlite3_stmt *ss; LY_ERR err; ss = db_prepare( "SELECT\n" " configuration\n" "FROM\n" " transactions\n" "WHERE\n" " rowid=?;"); if (!ss) return NULL; if (db_bindf(ss, "%d", transaction_id) != 0) goto exit; if (db_run(ss) != SQLITE_ROW) goto exit; if (db_loadf(ss, "%s", &config_str) != 0) goto exit; err = lyd_parse_data_mem(ly_native_ctx, config_str, LYD_XML, LYD_PARSE_STRICT | LYD_PARSE_NO_STATE, LYD_VALIDATE_NO_STATE, &dnode); if (err || !dnode) flog_warn(EC_LIB_LIBYANG, "%s: lyd_parse_data_mem() failed", __func__); else config = nb_config_new(dnode); exit: db_finalize(&ss); #endif /* HAVE_CONFIG_ROLLBACKS */ return config; } int nb_db_clear_transactions(unsigned int n_oldest) { #ifdef HAVE_CONFIG_ROLLBACKS /* Delete oldest N entries. */ if (db_execute("DELETE\n" "FROM\n" " transactions\n" "WHERE\n" " ROWID IN (\n" " SELECT\n" " ROWID\n" " FROM\n" " transactions\n" " ORDER BY ROWID ASC LIMIT %u\n" " );", n_oldest) != 0) return NB_ERR; #endif /* HAVE_CONFIG_ROLLBACKS */ return NB_OK; } int nb_db_set_max_transactions(unsigned int max) { #ifdef HAVE_CONFIG_ROLLBACKS /* * Delete old entries if necessary and update the SQL trigger that * auto-deletes old entries. */ if (db_execute("BEGIN TRANSACTION;\n" " DELETE\n" " FROM\n" " transactions\n" " WHERE\n" " ROWID IN (\n" " SELECT\n" " ROWID\n" " FROM\n" " transactions\n" " ORDER BY ROWID DESC LIMIT -1 OFFSET %u\n" " );\n" " DROP TRIGGER delete_tail;\n" " CREATE TRIGGER delete_tail\n" " AFTER INSERT ON transactions\n" " FOR EACH ROW\n" " BEGIN\n" " DELETE\n" " FROM\n" " transactions\n" " WHERE\n" " rowid%%%u=NEW.rowid%%%u AND rowid!=NEW.rowid;\n" " END;\n" "COMMIT;", max, max, max) != 0) return NB_ERR; #endif /* HAVE_CONFIG_ROLLBACKS */ return NB_OK; } int nb_db_transactions_iterate(void (*func)(void *arg, int transaction_id, const char *client_name, const char *date, const char *comment), void *arg) { #ifdef HAVE_CONFIG_ROLLBACKS struct sqlite3_stmt *ss; /* Send SQL query and parse the result. */ ss = db_prepare( "SELECT\n" " rowid, client, date, comment\n" "FROM\n" " transactions\n" "ORDER BY\n" " rowid DESC;"); if (!ss) return NB_ERR; while (db_run(ss) == SQLITE_ROW) { int transaction_id; const char *client_name; const char *date; const char *comment; int ret; ret = db_loadf(ss, "%i%s%s%s", &transaction_id, &client_name, &date, &comment); if (ret != 0) continue; (*func)(arg, transaction_id, client_name, date, comment); } db_finalize(&ss); #endif /* HAVE_CONFIG_ROLLBACKS */ return NB_OK; }