add database schema versioning to the HLR database

Make use of pragma user_version to store our database schema version.
The present schema is now identitifed as 'version 0', which is also
the default value for databases on which we never ran the statement
'pragma user_version' before.

Only bootstrap the database if it hasn't been bootstrapped yet.
Previously, bootstrap SQL statements ran every time osmo-hlr
opened the database, and any errors were being ignored in SQL.
Instead, we now first run a query which checks whether tables
already exist, and only create them if necessary.
This change will allow future schema updates to work properly.

Prepare for future schema upgrades by adding a new command-line
option which enables upgrades. This option defaults to 'false'
in order to avoid accidental upgrades.

Change-Id: I8aeaa9a404b622657cbc7138106f38aa6ad8d01b
Related: OS#2838
This commit is contained in:
Stefan Sperling 2018-11-27 12:10:45 +01:00 committed by Neels Hofmeyr
parent a820ea1f67
commit 8f3a7cce80
6 changed files with 128 additions and 25 deletions

View File

@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS subscriber (
CREATE TABLE subscriber (
-- OsmoHLR's DB scheme is modelled roughly after TS 23.008 version 13.3.0
id INTEGER PRIMARY KEY,
-- Chapter 2.1.1.1
@ -39,24 +39,24 @@ CREATE TABLE IF NOT EXISTS subscriber (
ms_purged_ps BOOLEAN NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS subscriber_apn (
CREATE TABLE subscriber_apn (
subscriber_id INTEGER, -- subscriber.id
apn VARCHAR(256) NOT NULL
);
CREATE TABLE IF NOT EXISTS subscriber_multi_msisdn (
CREATE TABLE subscriber_multi_msisdn (
-- Chapter 2.1.3
subscriber_id INTEGER, -- subscriber.id
msisdn VARCHAR(15) NOT NULL
);
CREATE TABLE IF NOT EXISTS auc_2g (
CREATE TABLE auc_2g (
subscriber_id INTEGER PRIMARY KEY, -- subscriber.id
algo_id_2g INTEGER NOT NULL, -- enum osmo_auth_algo value
ki VARCHAR(32) NOT NULL -- hex string: subscriber's secret key (128bit)
);
CREATE TABLE IF NOT EXISTS auc_3g (
CREATE TABLE auc_3g (
subscriber_id INTEGER PRIMARY KEY, -- subscriber.id
algo_id_3g INTEGER NOT NULL, -- enum osmo_auth_algo value
k VARCHAR(32) NOT NULL, -- hex string: subscriber's secret key (128bit)
@ -66,4 +66,7 @@ CREATE TABLE IF NOT EXISTS auc_3g (
ind_bitlen INTEGER NOT NULL DEFAULT 5 -- nr of index bits at lower SQN end
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_subscr_imsi ON subscriber (imsi);
CREATE UNIQUE INDEX idx_subscr_imsi ON subscriber (imsi);
-- Set HLR database schema version number
PRAGMA user_version = 0;

112
src/db.c
View File

@ -27,6 +27,8 @@
#include "db.h"
#include "db_bootstrap.h"
#define CURRENT_SCHEMA_VERSION 0
#define SEL_COLUMNS \
"id," \
"imsi," \
@ -197,36 +199,90 @@ static int db_bootstrap(struct db_context *dbc)
for (i = 0; i < ARRAY_SIZE(stmt_bootstrap_sql); i++) {
int rc;
sqlite3_stmt *stmt;
rc = sqlite3_prepare_v2(dbc->db, stmt_bootstrap_sql[i], -1,
&stmt, NULL);
rc = sqlite3_prepare_v2(dbc->db, stmt_bootstrap_sql[i], -1, &stmt, NULL);
if (rc != SQLITE_OK) {
LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n",
stmt_bootstrap_sql[i]);
LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", stmt_bootstrap_sql[i]);
return rc;
}
/* execute the statement */
rc = sqlite3_step(stmt);
db_remove_reset(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
LOGP(DDB, LOGL_ERROR, "Cannot bootstrap database: SQL error: (%d) %s,"
" during stmt '%s'",
rc, sqlite3_errmsg(dbc->db),
stmt_bootstrap_sql[i]);
rc, sqlite3_errmsg(dbc->db), stmt_bootstrap_sql[i]);
return rc;
}
}
return SQLITE_OK;
}
struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite_logging)
/* https://www.sqlite.org/fileformat2.html#storage_of_the_sql_database_schema */
static bool db_table_exists(struct db_context *dbc, const char *table_name)
{
const char *table_exists_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=?";
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_prepare_v2(dbc->db, table_exists_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", table_exists_sql);
return false;
}
if (!db_bind_text(stmt, NULL, table_name))
return false;
rc = sqlite3_step(stmt);
db_remove_reset(stmt);
sqlite3_finalize(stmt);
return (rc == SQLITE_ROW);
}
/* Indicate whether the database is initialized with tables for schema version 0.
* We only check for the 'subscriber' table here because Neels said so. */
static bool db_is_bootstrapped_v0(struct db_context *dbc)
{
if (!db_table_exists(dbc, "subscriber")) {
LOGP(DDB, LOGL_DEBUG, "Table 'subscriber' not found in database '%s'\n", dbc->fname);
return false;
}
return true;
}
static int db_get_user_version(struct db_context *dbc)
{
const char *user_version_sql = "PRAGMA user_version";
sqlite3_stmt *stmt;
int version, rc;
rc = sqlite3_prepare_v2(dbc->db, user_version_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", user_version_sql);
return -1;
}
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
version = sqlite3_column_int(stmt, 0);
} else {
LOGP(DDB, LOGL_ERROR, "SQL statement '%s' failed: %d\n", user_version_sql, rc);
version = -1;
}
db_remove_reset(stmt);
sqlite3_finalize(stmt);
return version;
}
struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite_logging, bool allow_upgrade)
{
struct db_context *dbc = talloc_zero(ctx, struct db_context);
unsigned int i;
int rc;
bool has_sqlite_config_sqllog = false;
int version;
LOGP(DDB, LOGL_NOTICE, "using database: %s\n", fname);
LOGP(DDB, LOGL_INFO, "Compiled against SQLite3 lib version %s\n", SQLITE_VERSION);
@ -275,10 +331,40 @@ struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite_logg
LOGP(DDB, LOGL_ERROR, "Unable to set Write-Ahead Logging: %s\n",
err_msg);
rc = db_bootstrap(dbc);
if (rc != SQLITE_OK) {
LOGP(DDB, LOGL_ERROR, "Failed to bootstrap DB: (rc=%d) %s\n",
rc, sqlite3_errmsg(dbc->db));
version = db_get_user_version(dbc);
if (version < 0) {
LOGP(DDB, LOGL_ERROR, "Unable to read user version number from database '%s'\n", dbc->fname);
goto out_free;
}
/* An empty database will always report version zero. */
if (version == 0 && !db_is_bootstrapped_v0(dbc)) {
LOGP(DDB, LOGL_NOTICE, "Missing database tables detected; Bootstrapping database '%s'\n", dbc->fname);
rc = db_bootstrap(dbc);
if (rc != SQLITE_OK) {
LOGP(DDB, LOGL_ERROR, "Failed to bootstrap DB: (rc=%d) %s\n",
rc, sqlite3_errmsg(dbc->db));
goto out_free;
}
}
LOGP(DDB, LOGL_NOTICE, "Database '%s' has HLR DB schema version %d\n", dbc->fname, version);
if (version < CURRENT_SCHEMA_VERSION && allow_upgrade) {
/* Future version upgrades will happen here. */
}
if (version != CURRENT_SCHEMA_VERSION) {
if (version < CURRENT_SCHEMA_VERSION) {
LOGP(DDB, LOGL_NOTICE, "HLR DB schema version %d is outdated\n", version);
if (!allow_upgrade) {
LOGP(DDB, LOGL_ERROR, "Not upgrading HLR database to schema version %d; "
"use the --db-upgrade option to allow HLR database upgrades\n",
CURRENT_SCHEMA_VERSION);
}
} else
LOGP(DDB, LOGL_ERROR, "HLR DB schema version %d is unknown\n", version);
goto out_free;
}

View File

@ -39,7 +39,7 @@ bool db_bind_text(sqlite3_stmt *stmt, const char *param_name, const char *text);
bool db_bind_int(sqlite3_stmt *stmt, const char *param_name, int nr);
bool db_bind_int64(sqlite3_stmt *stmt, const char *param_name, int64_t nr);
void db_close(struct db_context *dbc);
struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite3_logging);
struct db_context *db_open(void *ctx, const char *fname, bool enable_sqlite3_logging, bool allow_upgrades);
#include <osmocom/crypt/auth.h>

View File

@ -483,6 +483,7 @@ static void print_help()
printf(" -s --disable-color Do not print ANSI colors in the log\n");
printf(" -T --timestamp Prefix every log line with a timestamp.\n");
printf(" -e --log-level number Set a global loglevel.\n");
printf(" -U --db-upgrade Allow HLR database schema upgrades.\n");
printf(" -V --version Print the version of OsmoHLR.\n");
}
@ -490,10 +491,12 @@ static struct {
const char *config_file;
const char *db_file;
bool daemonize;
bool db_upgrade;
} cmdline_opts = {
.config_file = "osmo-hlr.cfg",
.db_file = "hlr.db",
.daemonize = false,
.db_upgrade = false,
};
static void handle_options(int argc, char **argv)
@ -509,11 +512,12 @@ static void handle_options(int argc, char **argv)
{"disable-color", 0, 0, 's'},
{"log-level", 1, 0, 'e'},
{"timestamp", 0, 0, 'T'},
{"db-upgrade", 0, 0, 'U' },
{"version", 0, 0, 'V' },
{0, 0, 0, 0}
};
c = getopt_long(argc, argv, "hc:l:d:Dse:TV",
c = getopt_long(argc, argv, "hc:l:d:Dse:TUV",
long_options, &option_index);
if (c == -1)
break;
@ -544,6 +548,9 @@ static void handle_options(int argc, char **argv)
case 'T':
log_set_print_timestamp(osmo_stderr_target, 1);
break;
case 'U':
cmdline_opts.db_upgrade = true;
break;
case 'V':
print_version(1);
exit(0);
@ -637,7 +644,7 @@ int main(int argc, char **argv)
exit(1);
}
g_hlr->dbc = db_open(hlr_ctx, cmdline_opts.db_file, true);
g_hlr->dbc = db_open(hlr_ctx, cmdline_opts.db_file, true, cmdline_opts.db_upgrade);
if (!g_hlr->dbc) {
LOGP(DMAIN, LOGL_FATAL, "Error opening database\n");
exit(1);

View File

@ -44,8 +44,10 @@ static struct {
const char *db_file;
bool bootstrap;
const char *import_nitb_db;
bool db_upgrade;
} cmdline_opts = {
.db_file = "hlr.db",
.db_upgrade = false,
};
static void print_help()
@ -59,6 +61,7 @@ static void print_help()
printf(" -s --disable-color Do not print ANSI colors in the log\n");
printf(" -T --timestamp Prefix every log line with a timestamp.\n");
printf(" -e --log-level number Set a global loglevel.\n");
printf(" -U --db-upgrade Allow HLR database schema upgrades.\n");
printf(" -V --version Print the version of OsmoHLR-db-tool.\n");
printf("\n");
printf("Commands:\n");
@ -96,11 +99,12 @@ static void handle_options(int argc, char **argv)
{"disable-color", 0, 0, 's'},
{"timestamp", 0, 0, 'T'},
{"log-level", 1, 0, 'e'},
{"db-upgrade", 0, 0, 'U' },
{"version", 0, 0, 'V' },
{0, 0, 0, 0}
};
c = getopt_long(argc, argv, "hl:d:sTe:V",
c = getopt_long(argc, argv, "hl:d:sTe:UV",
long_options, &option_index);
if (c == -1)
break;
@ -124,6 +128,9 @@ static void handle_options(int argc, char **argv)
case 'e':
log_set_log_level(osmo_stderr_target, atoi(optarg));
break;
case 'U':
cmdline_opts.db_upgrade = true;
break;
case 'V':
print_version(1);
exit(EXIT_SUCCESS);
@ -409,7 +416,7 @@ int main(int argc, char **argv)
exit(EXIT_FAILURE);
}
g_hlr_db_tool_ctx->dbc = db_open(g_hlr_db_tool_ctx, cmdline_opts.db_file, true);
g_hlr_db_tool_ctx->dbc = db_open(g_hlr_db_tool_ctx, cmdline_opts.db_file, true, cmdline_opts.db_upgrade);
if (!g_hlr_db_tool_ctx->dbc) {
LOGP(DMAIN, LOGL_FATAL, "Error opening database\n");
exit(EXIT_FAILURE);

View File

@ -850,7 +850,7 @@ int main(int argc, char **argv)
log_set_log_level(osmo_stderr_target, LOGL_ERROR);
/* Disable SQLite logging so that we're not vulnerable on SQLite error messages changing across
* library versions. */
dbc = db_open(ctx, "db_test.db", false);
dbc = db_open(ctx, "db_test.db", false, false);
log_set_log_level(osmo_stderr_target, 0);
OSMO_ASSERT(dbc);