/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 2006, Proformatique * * Written by Richard Braun * * Based on res_sqlite3 by Anthony Minessale II, * and res_config_mysql by Matthew Boehm * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ /*! * \page res_config_sqlite * * \section intro_sec Presentation * * res_config_sqlite is a module for the Asterisk Open Source PBX to * support SQLite 2 databases. It can be used to fetch configuration * from a database (static configuration files and/or using the Asterisk * RealTime Architecture - ARA). It can also be used to log CDR entries. * Note that Asterisk already comes with a module named cdr_sqlite. * There are two reasons for including it in res_config_sqlite: * the first is that rewriting it was a training to learn how to write a * simple module for Asterisk, the other is to have the same database open for * all kinds of operations, which improves reliability and performance. * * \section conf_sec Configuration * * The main configuration file is res_config_sqlite.conf.sample It must be readable or * res_config_sqlite will fail to start. It is suggested to use the sample file * in this package as a starting point. The file has only one section * named general. Here are the supported parameters : * *
*
dbfile
*
The absolute path to the SQLite database (the file can be non existent, * res_config_sqlite will create it if it has the appropriate rights)
*
config_table
*
The table used for static configuration
*
cdr_table
*
The table used to store CDR entries (if ommitted, CDR support is * disabled)
*
* * To use res_config_sqlite for static and/or RealTime configuration, refer to the * Asterisk documentation. The file tables.sql can be used to create the * needed tables. * * \section status_sec Driver status * * The CLI command show sqlite status returns status information * about the running driver. * * \section credits_sec Credits * * res_config_sqlite was developed by Richard Braun at the Proformatique company. */ /*! * \file * \brief res_config_sqlite module. */ /*! \li \ref res_config_sqlite.c uses the configuration file \ref res_config_sqlite.conf * \addtogroup configuration_file Configuration Files */ /*! * \page res_config_sqlite.conf res_config_sqlite.conf * \verbinclude res_config_sqlite.conf.sample */ /*** MODULEINFO sqlite extended ***/ #include "asterisk.h" ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include #include "asterisk/logger.h" #include "asterisk/app.h" #include "asterisk/pbx.h" #include "asterisk/cdr.h" #include "asterisk/cli.h" #include "asterisk/lock.h" #include "asterisk/config.h" #include "asterisk/module.h" #include "asterisk/linkedlists.h" #define MACRO_BEGIN do { #define MACRO_END } while (0) #define RES_CONFIG_SQLITE_NAME "res_config_sqlite" #define RES_CONFIG_SQLITE_DRIVER "sqlite" #define RES_CONFIG_SQLITE_DESCRIPTION "Resource Module for SQLite 2" #define RES_CONFIG_SQLITE_CONF_FILE "res_config_sqlite.conf" enum { RES_CONFIG_SQLITE_CONFIG_ID, RES_CONFIG_SQLITE_CONFIG_CAT_METRIC, RES_CONFIG_SQLITE_CONFIG_VAR_METRIC, RES_CONFIG_SQLITE_CONFIG_COMMENTED, RES_CONFIG_SQLITE_CONFIG_FILENAME, RES_CONFIG_SQLITE_CONFIG_CATEGORY, RES_CONFIG_SQLITE_CONFIG_VAR_NAME, RES_CONFIG_SQLITE_CONFIG_VAR_VAL, RES_CONFIG_SQLITE_CONFIG_COLUMNS, }; #define SET_VAR(config, to, from) \ MACRO_BEGIN \ int __error; \ \ __error = set_var(&to, #to, from->value); \ \ if (__error) { \ ast_config_destroy(config); \ unload_config(); \ return 1; \ } \ MACRO_END AST_THREADSTORAGE(sql_buf); AST_THREADSTORAGE(where_buf); /*! * Maximum number of loops before giving up executing a query. Calls to * sqlite_xxx() functions which can return SQLITE_BUSY * are enclosed by RES_CONFIG_SQLITE_BEGIN and RES_CONFIG_SQLITE_END, e.g. *
 * char *errormsg;
 * int error;
 *
 * RES_CONFIG_SQLITE_BEGIN
 *	 error = sqlite_exec(db, query, NULL, NULL, &errormsg);
 * RES_CONFIG_SQLITE_END(error)
 *
 * if (error)
 *	 ...;
 * 
*/ #define RES_CONFIG_SQLITE_MAX_LOOPS 10 /*! * Macro used before executing a query. * * \see RES_CONFIG_SQLITE_MAX_LOOPS. */ #define RES_CONFIG_SQLITE_BEGIN \ MACRO_BEGIN \ int __i; \ \ for (__i = 0; __i < RES_CONFIG_SQLITE_MAX_LOOPS; __i++) { /*! * Macro used after executing a query. * * \see RES_CONFIG_SQLITE_MAX_LOOPS. */ #define RES_CONFIG_SQLITE_END(error) \ if (error != SQLITE_BUSY) \ break; \ usleep(1000); \ } \ MACRO_END; /*! * Structure sent to the SQLite callback function for static configuration. * * \see add_cfg_entry() */ struct cfg_entry_args { struct ast_config *cfg; struct ast_category *cat; char *cat_name; struct ast_flags flags; const char *who_asked; }; /*! * Structure sent to the SQLite callback function for RealTime configuration. * * \see add_rt_cfg_entry() */ struct rt_cfg_entry_args { struct ast_variable *var; struct ast_variable *last; }; /*! * Structure sent to the SQLite callback function for RealTime configuration * (realtime_multi_handler()). * * \see add_rt_multi_cfg_entry() */ struct rt_multi_cfg_entry_args { struct ast_config *cfg; char *initfield; }; /*! * \brief Allocate a variable. * \param var the address of the variable to set (it will be allocated) * \param name the name of the variable (for error handling) * \param value the value to store in var * \retval 0 on success * \retval 1 if an allocation error occurred */ static int set_var(char **var, const char *name, const char *value); /*! * \brief Load the configuration file. * \see unload_config() * * This function sets dbfile, config_table, and cdr_table. It calls * check_vars() before returning, and unload_config() if an error occurred. * * \retval 0 on success * \retval 1 if an error occurred */ static int load_config(void); /*! * \brief Free resources related to configuration. * \see load_config() */ static void unload_config(void); /*! * \brief Asterisk callback function for CDR support. * \param cdr the CDR entry Asterisk sends us. * * Asterisk will call this function each time a CDR entry must be logged if * CDR support is enabled. * * \retval 0 on success * \retval 1 if an error occurred */ static int cdr_handler(struct ast_cdr *cdr); /*! * \brief SQLite callback function for static configuration. * * This function is passed to the SQLite engine as a callback function to * parse a row and store it in a struct ast_config object. It relies on * resulting rows being sorted by category. * * \param arg a pointer to a struct cfg_entry_args object * \param argc number of columns * \param argv values in the row * \param columnNames names and types of the columns * \retval 0 on success * \retval 1 if an error occurred * \see cfg_entry_args * \see sql_get_config_table * \see config_handler() */ static int add_cfg_entry(void *arg, int argc, char **argv, char **columnNames); /*! * \brief Asterisk callback function for static configuration. * * Asterisk will call this function when it loads its static configuration, * which usually happens at startup and reload. * * \param database the database to use (ignored) * \param table the table to use * \param file the file to load from the database * \param cfg the struct ast_config object to use when storing variables * \param flags Optional flags. Not used. * \param suggested_incl suggest include. * \param who_asked * \retval cfg object * \retval NULL if an error occurred * \see add_cfg_entry() */ static struct ast_config * config_handler(const char *database, const char *table, const char *file, struct ast_config *cfg, struct ast_flags flags, const char *suggested_incl, const char *who_asked); /*! * \brief SQLite callback function for RealTime configuration. * * This function is passed to the SQLite engine as a callback function to * parse a row and store it in a linked list of struct ast_variable objects. * * \param arg a pointer to a struct rt_cfg_entry_args object * \param argc number of columns * \param argv values in the row * \param columnNames names and types of the columns * \retval 0 on success. * \retval 1 if an error occurred. * \see rt_cfg_entry_args * \see realtime_handler() */ static int add_rt_cfg_entry(void *arg, int argc, char **argv, char **columnNames); /*! * \brief Asterisk callback function for RealTime configuration. * * Asterisk will call this function each time it requires a variable * through the RealTime architecture. ap is a list of parameters and * values used to find a specific row, e.g one parameter "name" and * one value "123" so that the SQL query becomes SELECT * FROM * table WHERE name = '123';. * * \param database the database to use (ignored) * \param table the table to use * \param fields list of parameters and values to match * * \retval a linked list of struct ast_variable objects * \retval NULL if an error occurred * \see add_rt_cfg_entry() */ static struct ast_variable * realtime_handler(const char *database, const char *table, const struct ast_variable *fields); /*! * \brief SQLite callback function for RealTime configuration. * * This function performs the same actions as add_rt_cfg_entry() except * that the rt_multi_cfg_entry_args structure is designed to store * categories in addition to variables. * * \param arg a pointer to a struct rt_multi_cfg_entry_args object * \param argc number of columns * \param argv values in the row * \param columnNames names and types of the columns * \retval 0 on success. * \retval 1 if an error occurred. * \see rt_multi_cfg_entry_args * \see realtime_multi_handler() */ static int add_rt_multi_cfg_entry(void *arg, int argc, char **argv, char **columnNames); /*! * \brief Asterisk callback function for RealTime configuration. * * This function performs the same actions as realtime_handler() except * that it can store variables per category, and can return several * categories. * * \param database the database to use (ignored) * \param table the table to use * \param fields list of parameters and values to match * \retval a struct ast_config object storing categories and variables. * \retval NULL if an error occurred. * * \see add_rt_multi_cfg_entry() */ static struct ast_config * realtime_multi_handler(const char *database, const char *table, const struct ast_variable *fields); /*! * \brief Asterisk callback function for RealTime configuration (variable * update). * * Asterisk will call this function each time a variable has been modified * internally and must be updated in the backend engine. keyfield and entity * are used to find the row to update, e.g. UPDATE table SET ... WHERE * keyfield = 'entity';. ap is a list of parameters and values with the * same format as the other realtime functions. * * \param database the database to use (ignored) * \param table the table to use * \param keyfield the column of the matching cell * \param entity the value of the matching cell * \param fields list of parameters and new values to update in the database * \retval the number of affected rows. * \retval -1 if an error occurred. */ static int realtime_update_handler(const char *database, const char *table, const char *keyfield, const char *entity, const struct ast_variable *fields); static int realtime_update2_handler(const char *database, const char *table, const struct ast_variable *lookup_fields, const struct ast_variable *update_fields); /*! * \brief Asterisk callback function for RealTime configuration (variable * create/store). * * Asterisk will call this function each time a variable has been created * internally and must be stored in the backend engine. * are used to find the row to update, e.g. ap is a list of parameters and * values with the same format as the other realtime functions. * * \param database the database to use (ignored) * \param table the table to use * \param fields list of parameters and new values to insert into the database * \retval the rowid of inserted row. * \retval -1 if an error occurred. */ static int realtime_store_handler(const char *database, const char *table, const struct ast_variable *fields); /*! * \brief Asterisk callback function for RealTime configuration (destroys * variable). * * Asterisk will call this function each time a variable has been destroyed * internally and must be removed from the backend engine. keyfield and entity * are used to find the row to delete, e.g. DELETE FROM table WHERE * keyfield = 'entity';. ap is a list of parameters and values with the * same format as the other realtime functions. * * \param database the database to use (ignored) * \param table the table to use * \param keyfield the column of the matching cell * \param entity the value of the matching cell * \param fields list of additional parameters for cell matching * \retval the number of affected rows. * \retval -1 if an error occurred. */ static int realtime_destroy_handler(const char *database, const char *table, const char *keyfield, const char *entity, const struct ast_variable *fields); /*! * \brief Asterisk callback function for the CLI status command. * * \param e CLI command * \param cmd * \param a CLI argument list * \return RESULT_SUCCESS */ static char *handle_cli_show_sqlite_status(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a); static char *handle_cli_sqlite_show_tables(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a); static int realtime_require_handler(const char *database, const char *table, va_list ap); static int realtime_unload_handler(const char *unused, const char *tablename); /*! The SQLite database object. */ static sqlite *db; /*! Set to 1 if CDR support is enabled. */ static int use_cdr; /*! Set to 1 if the CDR callback function was registered. */ static int cdr_registered; /*! Set to 1 if the CLI status command callback function was registered. */ static int cli_status_registered; /*! The path of the database file. */ static char *dbfile; /*! The name of the static configuration table. */ static char *config_table; /*! The name of the table used to store CDR entries. */ static char *cdr_table; /*! * The structure specifying all callback functions used by Asterisk for static * and RealTime configuration. */ static struct ast_config_engine sqlite_engine = { .name = RES_CONFIG_SQLITE_DRIVER, .load_func = config_handler, .realtime_func = realtime_handler, .realtime_multi_func = realtime_multi_handler, .store_func = realtime_store_handler, .destroy_func = realtime_destroy_handler, .update_func = realtime_update_handler, .update2_func = realtime_update2_handler, .require_func = realtime_require_handler, .unload_func = realtime_unload_handler, }; /*! * The mutex used to prevent simultaneous access to the SQLite database. */ AST_MUTEX_DEFINE_STATIC(mutex); /*! * Structure containing details and callback functions for the CLI status * command. */ static struct ast_cli_entry cli_status[] = { AST_CLI_DEFINE(handle_cli_show_sqlite_status, "Show status information about the SQLite 2 driver"), AST_CLI_DEFINE(handle_cli_sqlite_show_tables, "Cached table information about the SQLite 2 driver"), }; struct sqlite_cache_columns { char *name; char *type; unsigned char isint; /*!< By definition, only INTEGER PRIMARY KEY is an integer; everything else is a string. */ AST_RWLIST_ENTRY(sqlite_cache_columns) list; }; struct sqlite_cache_tables { char *name; AST_RWLIST_HEAD(_columns, sqlite_cache_columns) columns; AST_RWLIST_ENTRY(sqlite_cache_tables) list; }; static AST_RWLIST_HEAD_STATIC(sqlite_tables, sqlite_cache_tables); /* * Taken from Asterisk 1.2 cdr_sqlite.so. */ /*! SQL query format to create the CDR table if non existent. */ static char *sql_create_cdr_table = "CREATE TABLE '%q' (\n" " id INTEGER,\n" " clid VARCHAR(80) NOT NULL DEFAULT '',\n" " src VARCHAR(80) NOT NULL DEFAULT '',\n" " dst VARCHAR(80) NOT NULL DEFAULT '',\n" " dcontext VARCHAR(80) NOT NULL DEFAULT '',\n" " channel VARCHAR(80) NOT NULL DEFAULT '',\n" " dstchannel VARCHAR(80) NOT NULL DEFAULT '',\n" " lastapp VARCHAR(80) NOT NULL DEFAULT '',\n" " lastdata VARCHAR(80) NOT NULL DEFAULT '',\n" " start DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',\n" " answer DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',\n" " end DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',\n" " duration INT(11) NOT NULL DEFAULT 0,\n" " billsec INT(11) NOT NULL DEFAULT 0,\n" " disposition VARCHAR(45) NOT NULL DEFAULT '',\n" " amaflags INT(11) NOT NULL DEFAULT 0,\n" " accountcode VARCHAR(20) NOT NULL DEFAULT '',\n" " uniqueid VARCHAR(32) NOT NULL DEFAULT '',\n" " userfield VARCHAR(255) NOT NULL DEFAULT '',\n" " PRIMARY KEY (id)\n" ");"; /*! * SQL query format to describe the table structure */ #define sql_table_structure "SELECT sql FROM sqlite_master WHERE type='table' AND tbl_name='%s'" /*! * SQL query format to fetch the static configuration of a file. * Rows must be sorted by category. * * \see add_cfg_entry() */ #define sql_get_config_table \ "SELECT *" \ " FROM '%q'" \ " WHERE filename = '%q' AND commented = 0" \ " ORDER BY cat_metric ASC, var_metric ASC;" static void free_table(struct sqlite_cache_tables *tblptr) { struct sqlite_cache_columns *col; /* Obtain a write lock to ensure there are no read locks outstanding */ AST_RWLIST_WRLOCK(&(tblptr->columns)); while ((col = AST_RWLIST_REMOVE_HEAD(&(tblptr->columns), list))) { ast_free(col); } AST_RWLIST_UNLOCK(&(tblptr->columns)); AST_RWLIST_HEAD_DESTROY(&(tblptr->columns)); ast_free(tblptr); } static int find_table_cb(void *vtblptr, int argc, char **argv, char **columnNames) { struct sqlite_cache_tables *tblptr = vtblptr; char *sql = ast_strdupa(argv[0]), *start, *end, *type, *remainder; int i; AST_DECLARE_APP_ARGS(fie, AST_APP_ARG(ld)[100]; /* This means we support up to 100 columns per table */ ); struct sqlite_cache_columns *col; /* This is really fun. We get to parse an SQL statement to figure out * what columns are in the table. */ if ((start = strchr(sql, '(')) && (end = strrchr(sql, ')'))) { start++; *end = '\0'; } else { /* Abort */ return -1; } AST_STANDARD_APP_ARGS(fie, start); for (i = 0; i < fie.argc; i++) { fie.ld[i] = ast_skip_blanks(fie.ld[i]); ast_debug(5, "Found field: %s\n", fie.ld[i]); if (strncasecmp(fie.ld[i], "PRIMARY KEY", 11) == 0 && (start = strchr(fie.ld[i], '(')) && (end = strchr(fie.ld[i], ')'))) { *end = '\0'; AST_RWLIST_TRAVERSE(&(tblptr->columns), col, list) { if (strcasecmp(start + 1, col->name) == 0 && strcasestr(col->type, "INTEGER")) { col->isint = 1; } } continue; } /* type delimiter could be any space character */ for (type = fie.ld[i]; *type > 32; type++); *type++ = '\0'; type = ast_skip_blanks(type); for (remainder = type; *remainder > 32; remainder++); *remainder = '\0'; if (!(col = ast_calloc(1, sizeof(*col) + strlen(fie.ld[i]) + strlen(type) + 2))) { return -1; } col->name = (char *)col + sizeof(*col); col->type = (char *)col + sizeof(*col) + strlen(fie.ld[i]) + 1; strcpy(col->name, fie.ld[i]); /* SAFE */ strcpy(col->type, type); /* SAFE */ if (strcasestr(col->type, "INTEGER") && strcasestr(col->type, "PRIMARY KEY")) { col->isint = 1; } AST_LIST_INSERT_TAIL(&(tblptr->columns), col, list); } return 0; } static struct sqlite_cache_tables *find_table(const char *tablename) { struct sqlite_cache_tables *tblptr; int i, err; char *sql, *errstr = NULL; AST_RWLIST_RDLOCK(&sqlite_tables); for (i = 0; i < 2; i++) { AST_RWLIST_TRAVERSE(&sqlite_tables, tblptr, list) { if (strcmp(tblptr->name, tablename) == 0) { break; } } if (tblptr) { AST_RWLIST_RDLOCK(&(tblptr->columns)); AST_RWLIST_UNLOCK(&sqlite_tables); return tblptr; } if (i == 0) { AST_RWLIST_UNLOCK(&sqlite_tables); AST_RWLIST_WRLOCK(&sqlite_tables); } } /* Table structure not cached; build the structure now */ if (ast_asprintf(&sql, sql_table_structure, tablename) < 0) { sql = NULL; } if (!(tblptr = ast_calloc(1, sizeof(*tblptr) + strlen(tablename) + 1))) { AST_RWLIST_UNLOCK(&sqlite_tables); ast_log(LOG_ERROR, "Memory error. Cannot cache table '%s'\n", tablename); ast_free(sql); return NULL; } tblptr->name = (char *)tblptr + sizeof(*tblptr); strcpy(tblptr->name, tablename); /* SAFE */ AST_RWLIST_HEAD_INIT(&(tblptr->columns)); ast_debug(1, "About to query table structure: %s\n", sql); ast_mutex_lock(&mutex); if ((err = sqlite_exec(db, sql, find_table_cb, tblptr, &errstr))) { ast_mutex_unlock(&mutex); ast_log(LOG_WARNING, "SQLite error %d: %s\n", err, errstr); ast_free(errstr); free_table(tblptr); AST_RWLIST_UNLOCK(&sqlite_tables); ast_free(sql); return NULL; } ast_mutex_unlock(&mutex); ast_free(sql); if (AST_LIST_EMPTY(&(tblptr->columns))) { free_table(tblptr); AST_RWLIST_UNLOCK(&sqlite_tables); return NULL; } AST_RWLIST_INSERT_TAIL(&sqlite_tables, tblptr, list); AST_RWLIST_RDLOCK(&(tblptr->columns)); AST_RWLIST_UNLOCK(&sqlite_tables); return tblptr; } #define release_table(a) AST_RWLIST_UNLOCK(&((a)->columns)) static int set_var(char **var, const char *name, const char *value) { if (*var) ast_free(*var); *var = ast_strdup(value); if (!*var) { ast_log(LOG_WARNING, "Unable to allocate variable %s\n", name); return 1; } return 0; } static int check_vars(void) { if (!dbfile) { ast_log(LOG_ERROR, "Required parameter undefined: dbfile\n"); return 1; } use_cdr = (cdr_table != NULL); return 0; } static int load_config(void) { struct ast_config *config; struct ast_variable *var; int error; struct ast_flags config_flags = { 0 }; config = ast_config_load(RES_CONFIG_SQLITE_CONF_FILE, config_flags); if (config == CONFIG_STATUS_FILEMISSING || config == CONFIG_STATUS_FILEINVALID) { ast_log(LOG_ERROR, "Unable to load " RES_CONFIG_SQLITE_CONF_FILE "\n"); return 1; } for (var = ast_variable_browse(config, "general"); var; var = var->next) { if (!strcasecmp(var->name, "dbfile")) SET_VAR(config, dbfile, var); else if (!strcasecmp(var->name, "config_table")) SET_VAR(config, config_table, var); else if (!strcasecmp(var->name, "cdr_table")) { SET_VAR(config, cdr_table, var); } else ast_log(LOG_WARNING, "Unknown parameter : %s\n", var->name); } ast_config_destroy(config); error = check_vars(); if (error) { unload_config(); return 1; } return 0; } static void unload_config(void) { struct sqlite_cache_tables *tbl; ast_free(dbfile); dbfile = NULL; ast_free(config_table); config_table = NULL; ast_free(cdr_table); cdr_table = NULL; AST_RWLIST_WRLOCK(&sqlite_tables); while ((tbl = AST_RWLIST_REMOVE_HEAD(&sqlite_tables, list))) { free_table(tbl); } AST_RWLIST_UNLOCK(&sqlite_tables); } static int cdr_handler(struct ast_cdr *cdr) { char *errormsg = NULL, *tmp, workspace[500]; int error, scannum; struct sqlite_cache_tables *tbl = find_table(cdr_table); struct sqlite_cache_columns *col; struct ast_str *sql1 = ast_str_create(160), *sql2 = ast_str_create(16); int first = 1; if (!sql1 || !sql2) { ast_free(sql1); ast_free(sql2); return -1; } if (!tbl) { ast_log(LOG_WARNING, "No such table: %s\n", cdr_table); ast_free(sql1); ast_free(sql2); return -1; } ast_str_set(&sql1, 0, "INSERT INTO %s (", cdr_table); ast_str_set(&sql2, 0, ") VALUES ("); AST_RWLIST_TRAVERSE(&(tbl->columns), col, list) { if (col->isint) { ast_cdr_format_var(cdr, col->name, &tmp, workspace, sizeof(workspace), 1); if (!tmp) { continue; } if (sscanf(tmp, "%30d", &scannum) == 1) { ast_str_append(&sql1, 0, "%s%s", first ? "" : ",", col->name); ast_str_append(&sql2, 0, "%s%d", first ? "" : ",", scannum); } } else { ast_cdr_format_var(cdr, col->name, &tmp, workspace, sizeof(workspace), 0); if (!tmp) { continue; } ast_str_append(&sql1, 0, "%s%s", first ? "" : ",", col->name); tmp = sqlite_mprintf("%Q", tmp); ast_str_append(&sql2, 0, "%s%s", first ? "" : ",", tmp); sqlite_freemem(tmp); } first = 0; } release_table(tbl); ast_str_append(&sql1, 0, "%s)", ast_str_buffer(sql2)); ast_free(sql2); ast_debug(1, "SQL query: %s\n", ast_str_buffer(sql1)); ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, ast_str_buffer(sql1), NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) ast_mutex_unlock(&mutex); ast_free(sql1); if (error) { ast_log(LOG_ERROR, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); return 1; } sqlite_freemem(errormsg); return 0; } static int add_cfg_entry(void *arg, int argc, char **argv, char **columnNames) { struct cfg_entry_args *args; struct ast_variable *var; if (argc != RES_CONFIG_SQLITE_CONFIG_COLUMNS) { ast_log(LOG_WARNING, "Corrupt table\n"); return 1; } args = arg; if (!strcmp(argv[RES_CONFIG_SQLITE_CONFIG_VAR_NAME], "#include")) { struct ast_config *cfg; char *val; val = argv[RES_CONFIG_SQLITE_CONFIG_VAR_VAL]; cfg = ast_config_internal_load(val, args->cfg, args->flags, "", args->who_asked); if (!cfg) { ast_log(LOG_WARNING, "Unable to include %s\n", val); return 1; } else { args->cfg = cfg; return 0; } } if (!args->cat_name || strcmp(args->cat_name, argv[RES_CONFIG_SQLITE_CONFIG_CATEGORY])) { args->cat = ast_category_new_dynamic(argv[RES_CONFIG_SQLITE_CONFIG_CATEGORY]); if (!args->cat) { return 1; } ast_free(args->cat_name); args->cat_name = ast_strdup(argv[RES_CONFIG_SQLITE_CONFIG_CATEGORY]); if (!args->cat_name) { ast_category_destroy(args->cat); return 1; } ast_category_append(args->cfg, args->cat); } var = ast_variable_new(argv[RES_CONFIG_SQLITE_CONFIG_VAR_NAME], argv[RES_CONFIG_SQLITE_CONFIG_VAR_VAL], ""); if (!var) { ast_log(LOG_WARNING, "Unable to allocate variable\n"); return 1; } ast_variable_append(args->cat, var); return 0; } static struct ast_config *config_handler(const char *database, const char *table, const char *file, struct ast_config *cfg, struct ast_flags flags, const char *suggested_incl, const char *who_asked) { struct cfg_entry_args args; char *query, *errormsg = NULL; int error; if (!config_table) { if (!table) { ast_log(LOG_ERROR, "Table name unspecified\n"); return NULL; } } else table = config_table; query = sqlite_mprintf(sql_get_config_table, table, file); if (!query) { ast_log(LOG_WARNING, "Unable to allocate SQL query\n"); return NULL; } ast_debug(1, "SQL query: %s\n", query); args.cfg = cfg; args.cat = NULL; args.cat_name = NULL; args.flags = flags; args.who_asked = who_asked; ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, add_cfg_entry, &args, &errormsg); RES_CONFIG_SQLITE_END(error) ast_mutex_unlock(&mutex); ast_free(args.cat_name); sqlite_freemem(query); if (error) { ast_log(LOG_ERROR, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); return NULL; } sqlite_freemem(errormsg); return cfg; } static int add_rt_cfg_entry(void *arg, int argc, char **argv, char **columnNames) { struct rt_cfg_entry_args *args; struct ast_variable *var; int i; args = arg; for (i = 0; i < argc; i++) { if (!argv[i]) continue; if (!(var = ast_variable_new(columnNames[i], argv[i], ""))) return 1; if (!args->var) args->var = var; if (!args->last) args->last = var; else { args->last->next = var; args->last = var; } } return 0; } static struct ast_variable * realtime_handler(const char *database, const char *table, const struct ast_variable *fields) { char *query, *errormsg = NULL, *op, *tmp_str; struct rt_cfg_entry_args args; const struct ast_variable *field = fields; int error; if (!table) { ast_log(LOG_WARNING, "Table name unspecified\n"); return NULL; } if (!fields) { return NULL; } op = (strchr(field->name, ' ') == NULL) ? " =" : ""; /* \cond DOXYGEN_CAN_PARSE_THIS */ #undef QUERY #define QUERY "SELECT * FROM '%q' WHERE%s %q%s '%q'" /* \endcond */ query = sqlite_mprintf(QUERY, table, (config_table && !strcmp(config_table, table)) ? " commented = 0 AND" : "", field->name, op, field->value); if (!query) { ast_log(LOG_WARNING, "Unable to allocate SQL query\n"); return NULL; } while ((field = field->next)) { op = (strchr(field->name, ' ') == NULL) ? " =" : ""; tmp_str = sqlite_mprintf("%s AND %q%s '%q'", query, field->name, op, field->value); sqlite_freemem(query); if (!tmp_str) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); return NULL; } query = tmp_str; } tmp_str = sqlite_mprintf("%s LIMIT 1;", query); sqlite_freemem(query); if (!tmp_str) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); return NULL; } query = tmp_str; ast_debug(1, "SQL query: %s\n", query); args.var = NULL; args.last = NULL; ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, add_rt_cfg_entry, &args, &errormsg); RES_CONFIG_SQLITE_END(error) ast_mutex_unlock(&mutex); sqlite_freemem(query); if (error) { ast_log(LOG_WARNING, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); ast_variables_destroy(args.var); return NULL; } sqlite_freemem(errormsg); return args.var; } static int add_rt_multi_cfg_entry(void *arg, int argc, char **argv, char **columnNames) { struct rt_multi_cfg_entry_args *args; struct ast_category *cat; struct ast_variable *var; char *cat_name; size_t i; args = arg; cat_name = NULL; /* * cat_name should always be set here, since initfield is forged from * params[0] in realtime_multi_handler(), which is a search parameter * of the SQL query. */ for (i = 0; i < argc; i++) { if (!strcmp(args->initfield, columnNames[i])) cat_name = argv[i]; } if (!cat_name) { ast_log(LOG_ERROR, "Bogus SQL results, cat_name is NULL !\n"); return 1; } cat = ast_category_new_dynamic(cat_name); if (!cat) { return 1; } ast_category_append(args->cfg, cat); for (i = 0; i < argc; i++) { if (!argv[i]) { continue; } if (!(var = ast_variable_new(columnNames[i], argv[i], ""))) { ast_log(LOG_WARNING, "Unable to allocate variable\n"); return 1; } ast_variable_append(cat, var); } return 0; } static struct ast_config *realtime_multi_handler(const char *database, const char *table, const struct ast_variable *fields) { char *query, *errormsg = NULL, *op, *tmp_str, *initfield; struct rt_multi_cfg_entry_args args; const struct ast_variable *field = fields; struct ast_config *cfg; int error; if (!table) { ast_log(LOG_WARNING, "Table name unspecified\n"); return NULL; } if (!fields) { return NULL; } if (!(cfg = ast_config_new())) { ast_log(LOG_WARNING, "Unable to allocate configuration structure\n"); return NULL; } if (!(initfield = ast_strdup(field->name))) { ast_config_destroy(cfg); return NULL; } tmp_str = strchr(initfield, ' '); if (tmp_str) *tmp_str = '\0'; op = (!strchr(field->name, ' ')) ? " =" : ""; /* * Asterisk sends us an already escaped string when searching for * "exten LIKE" (uh!). Handle it separately. */ tmp_str = (!strcmp(field->value, "\\_%")) ? "_%" : (char *)field->value; /* \cond DOXYGEN_CAN_PARSE_THIS */ #undef QUERY #define QUERY "SELECT * FROM '%q' WHERE%s %q%s '%q'" /* \endcond */ if (!(query = sqlite_mprintf(QUERY, table, (config_table && !strcmp(config_table, table)) ? " commented = 0 AND" : "", field->name, op, tmp_str))) { ast_log(LOG_WARNING, "Unable to allocate SQL query\n"); ast_config_destroy(cfg); ast_free(initfield); return NULL; } while ((field = field->next)) { op = (!strchr(field->name, ' ')) ? " =" : ""; tmp_str = sqlite_mprintf("%s AND %q%s '%q'", query, field->name, op, field->value); sqlite_freemem(query); if (!tmp_str) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); ast_config_destroy(cfg); ast_free(initfield); return NULL; } query = tmp_str; } if (!(tmp_str = sqlite_mprintf("%s ORDER BY %q;", query, initfield))) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); sqlite_freemem(query); ast_config_destroy(cfg); ast_free(initfield); return NULL; } sqlite_freemem(query); query = tmp_str; ast_debug(1, "SQL query: %s\n", query); args.cfg = cfg; args.initfield = initfield; ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, add_rt_multi_cfg_entry, &args, &errormsg); RES_CONFIG_SQLITE_END(error) ast_mutex_unlock(&mutex); sqlite_freemem(query); ast_free(initfield); if (error) { ast_log(LOG_WARNING, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); ast_config_destroy(cfg); return NULL; } sqlite_freemem(errormsg); return cfg; } static int realtime_update_handler(const char *database, const char *table, const char *keyfield, const char *entity, const struct ast_variable *fields) { char *query, *errormsg = NULL, *tmp_str; const struct ast_variable *field = fields; int error, rows_num; if (!table) { ast_log(LOG_WARNING, "Table name unspecified\n"); return -1; } if (!field) { return -1; } /* \cond DOXYGEN_CAN_PARSE_THIS */ #undef QUERY #define QUERY "UPDATE '%q' SET %q = '%q'" /* \endcond */ if (!(query = sqlite_mprintf(QUERY, table, field->name, field->value))) { ast_log(LOG_WARNING, "Unable to allocate SQL query\n"); return -1; } while ((field = field->next)) { tmp_str = sqlite_mprintf("%s, %q = '%q'", query, field->name, field->value); sqlite_freemem(query); if (!tmp_str) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); return -1; } query = tmp_str; } if (!(tmp_str = sqlite_mprintf("%s WHERE %q = '%q';", query, keyfield, entity))) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); sqlite_freemem(query); return -1; } sqlite_freemem(query); query = tmp_str; ast_debug(1, "SQL query: %s\n", query); ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) if (!error) rows_num = sqlite_changes(db); else rows_num = -1; ast_mutex_unlock(&mutex); sqlite_freemem(query); if (error) { ast_log(LOG_WARNING, "%s\n", S_OR(errormsg, sqlite_error_string(error))); } sqlite_freemem(errormsg); return rows_num; } static int realtime_update2_handler(const char *database, const char *table, const struct ast_variable *lookup_fields, const struct ast_variable *update_fields) { char *errormsg = NULL, *tmp1, *tmp2; int error, rows_num, first = 1; struct ast_str *sql = ast_str_thread_get(&sql_buf, 100); struct ast_str *where = ast_str_thread_get(&where_buf, 100); const struct ast_variable *field; if (!table) { ast_log(LOG_WARNING, "Table name unspecified\n"); return -1; } if (!sql) { return -1; } ast_str_set(&sql, 0, "UPDATE %s SET", table); ast_str_set(&where, 0, " WHERE"); for (field = lookup_fields; field; field = field->next) { ast_str_append(&where, 0, "%s %s = %s", first ? "" : " AND", tmp1 = sqlite_mprintf("%q", field->name), tmp2 = sqlite_mprintf("%Q", field->value)); sqlite_freemem(tmp1); sqlite_freemem(tmp2); first = 0; } if (first) { ast_log(LOG_ERROR, "No criteria specified on update to '%s@%s'!\n", table, database); return -1; } first = 1; for (field = update_fields; field; field = field->next) { ast_str_append(&sql, 0, "%s %s = %s", first ? "" : ",", tmp1 = sqlite_mprintf("%q", field->name), tmp2 = sqlite_mprintf("%Q", field->value)); sqlite_freemem(tmp1); sqlite_freemem(tmp2); first = 0; } ast_str_append(&sql, 0, " %s", ast_str_buffer(where)); ast_debug(1, "SQL query: %s\n", ast_str_buffer(sql)); ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, ast_str_buffer(sql), NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) if (!error) { rows_num = sqlite_changes(db); } else { rows_num = -1; } ast_mutex_unlock(&mutex); if (error) { ast_log(LOG_WARNING, "%s\n", S_OR(errormsg, sqlite_error_string(error))); } sqlite_freemem(errormsg); return rows_num; } static int realtime_store_handler(const char *database, const char *table, const struct ast_variable *fields) { char *errormsg = NULL, *tmp_str, *tmp_keys = NULL, *tmp_keys2 = NULL, *tmp_vals = NULL, *tmp_vals2 = NULL; const struct ast_variable *field = fields; int error, rows_id; if (!table) { ast_log(LOG_WARNING, "Table name unspecified\n"); return -1; } if (!fields) { return -1; } /* \cond DOXYGEN_CAN_PARSE_THIS */ #undef QUERY #define QUERY "INSERT into '%q' (%s) VALUES (%s);" /* \endcond */ do { if ( tmp_keys2 ) { tmp_keys = sqlite_mprintf("%s, %q", tmp_keys2, field->name); sqlite_freemem(tmp_keys2); } else { tmp_keys = sqlite_mprintf("%q", field->name); } if (!tmp_keys) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); sqlite_freemem(tmp_vals); return -1; } if ( tmp_vals2 ) { tmp_vals = sqlite_mprintf("%s, '%q'", tmp_vals2, field->value); sqlite_freemem(tmp_vals2); } else { tmp_vals = sqlite_mprintf("'%q'", field->value); } if (!tmp_vals) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); sqlite_freemem(tmp_keys); return -1; } tmp_keys2 = tmp_keys; tmp_vals2 = tmp_vals; } while ((field = field->next)); if (!(tmp_str = sqlite_mprintf(QUERY, table, tmp_keys, tmp_vals))) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); sqlite_freemem(tmp_keys); sqlite_freemem(tmp_vals); return -1; } sqlite_freemem(tmp_keys); sqlite_freemem(tmp_vals); ast_debug(1, "SQL query: %s\n", tmp_str); ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, tmp_str, NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) if (!error) { rows_id = sqlite_last_insert_rowid(db); } else { rows_id = -1; } ast_mutex_unlock(&mutex); sqlite_freemem(tmp_str); if (error) { ast_log(LOG_WARNING, "%s\n", S_OR(errormsg, sqlite_error_string(error))); } sqlite_freemem(errormsg); return rows_id; } static int realtime_destroy_handler(const char *database, const char *table, const char *keyfield, const char *entity, const struct ast_variable *fields) { char *query, *errormsg = NULL, *tmp_str; const struct ast_variable *field; int error, rows_num; if (!table) { ast_log(LOG_WARNING, "Table name unspecified\n"); return -1; } /* \cond DOXYGEN_CAN_PARSE_THIS */ #undef QUERY #define QUERY "DELETE FROM '%q' WHERE" /* \endcond */ if (!(query = sqlite_mprintf(QUERY, table))) { ast_log(LOG_WARNING, "Unable to allocate SQL query\n"); return -1; } for (field = fields; field; field = field->next) { tmp_str = sqlite_mprintf("%s %q = '%q' AND", query, field->name, field->value); sqlite_freemem(query); if (!tmp_str) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); return -1; } query = tmp_str; } if (!(tmp_str = sqlite_mprintf("%s %q = '%q';", query, keyfield, entity))) { ast_log(LOG_WARNING, "Unable to reallocate SQL query\n"); sqlite_freemem(query); return -1; } sqlite_freemem(query); query = tmp_str; ast_debug(1, "SQL query: %s\n", query); ast_mutex_lock(&mutex); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) if (!error) { rows_num = sqlite_changes(db); } else { rows_num = -1; } ast_mutex_unlock(&mutex); sqlite_freemem(query); if (error) { ast_log(LOG_WARNING, "%s\n", S_OR(errormsg, sqlite_error_string(error))); } sqlite_freemem(errormsg); return rows_num; } static int realtime_require_handler(const char *unused, const char *tablename, va_list ap) { struct sqlite_cache_tables *tbl = find_table(tablename); struct sqlite_cache_columns *col; char *elm; int type, res = 0; if (!tbl) { return -1; } while ((elm = va_arg(ap, char *))) { type = va_arg(ap, require_type); va_arg(ap, int); /* Check if the field matches the criteria */ AST_RWLIST_TRAVERSE(&tbl->columns, col, list) { if (strcmp(col->name, elm) == 0) { /* SQLite only has two types - the 32-bit integer field that * is the key column, and everything else (everything else * being a string). */ if (col->isint && !ast_rq_is_int(type)) { ast_log(LOG_WARNING, "Realtime table %s: column '%s' is an integer field, but Asterisk requires that it not be!\n", tablename, col->name); res = -1; } break; } } if (!col) { ast_log(LOG_WARNING, "Realtime table %s requires column '%s', but that column does not exist!\n", tablename, elm); } } AST_RWLIST_UNLOCK(&(tbl->columns)); return res; } static int realtime_unload_handler(const char *unused, const char *tablename) { struct sqlite_cache_tables *tbl; AST_RWLIST_WRLOCK(&sqlite_tables); AST_RWLIST_TRAVERSE_SAFE_BEGIN(&sqlite_tables, tbl, list) { if (!strcasecmp(tbl->name, tablename)) { AST_RWLIST_REMOVE_CURRENT(list); free_table(tbl); } } AST_RWLIST_TRAVERSE_SAFE_END AST_RWLIST_UNLOCK(&sqlite_tables); return 0; } static char *handle_cli_show_sqlite_status(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { switch (cmd) { case CLI_INIT: e->command = "sqlite show status"; e->usage = "Usage: sqlite show status\n" " Show status information about the SQLite 2 driver\n"; return NULL; case CLI_GENERATE: return NULL; } if (a->argc != 3) return CLI_SHOWUSAGE; ast_cli(a->fd, "SQLite database path: %s\n", dbfile); ast_cli(a->fd, "config_table: "); if (!config_table) ast_cli(a->fd, "unspecified, must be present in extconfig.conf\n"); else ast_cli(a->fd, "%s\n", config_table); ast_cli(a->fd, "cdr_table: "); if (!cdr_table) ast_cli(a->fd, "unspecified, CDR support disabled\n"); else ast_cli(a->fd, "%s\n", cdr_table); return CLI_SUCCESS; } static char *handle_cli_sqlite_show_tables(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) { struct sqlite_cache_tables *tbl; struct sqlite_cache_columns *col; int found = 0; switch (cmd) { case CLI_INIT: e->command = "sqlite show tables"; e->usage = "Usage: sqlite show tables\n" " Show table information about the SQLite 2 driver\n"; return NULL; case CLI_GENERATE: return NULL; } if (a->argc != 3) return CLI_SHOWUSAGE; AST_RWLIST_RDLOCK(&sqlite_tables); AST_RWLIST_TRAVERSE(&sqlite_tables, tbl, list) { found++; ast_cli(a->fd, "Table %s:\n", tbl->name); AST_RWLIST_TRAVERSE(&(tbl->columns), col, list) { fprintf(stderr, "%s\n", col->name); ast_cli(a->fd, " %20.20s %-30.30s\n", col->name, col->type); } } AST_RWLIST_UNLOCK(&sqlite_tables); if (!found) { ast_cli(a->fd, "No tables currently in cache\n"); } return CLI_SUCCESS; } static int unload_module(void) { if (cdr_registered && ast_cdr_unregister(RES_CONFIG_SQLITE_NAME)) { return -1; } if (cli_status_registered) { ast_cli_unregister_multiple(cli_status, ARRAY_LEN(cli_status)); } ast_config_engine_deregister(&sqlite_engine); if (db) sqlite_close(db); unload_config(); return 0; } /*! * \brief Load the module * * Module loading including tests for configuration or dependencies. * This function can return AST_MODULE_LOAD_FAILURE, AST_MODULE_LOAD_DECLINE, * or AST_MODULE_LOAD_SUCCESS. If a dependency or environment variable fails * tests return AST_MODULE_LOAD_FAILURE. If the module can not load the * configuration file or other non-critical problem return * AST_MODULE_LOAD_DECLINE. On success return AST_MODULE_LOAD_SUCCESS. */ static int load_module(void) { char *errormsg = NULL; int error; db = NULL; cdr_registered = 0; cli_status_registered = 0; dbfile = NULL; config_table = NULL; cdr_table = NULL; error = load_config(); if (error) return AST_MODULE_LOAD_DECLINE; if (!(db = sqlite_open(dbfile, 0660, &errormsg))) { ast_log(LOG_ERROR, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); unload_module(); return AST_MODULE_LOAD_DECLINE; } sqlite_freemem(errormsg); errormsg = NULL; ast_config_engine_register(&sqlite_engine); if (use_cdr) { char *query; /* \cond DOXYGEN_CAN_PARSE_THIS */ #undef QUERY #define QUERY "SELECT COUNT(id) FROM %Q;" /* \endcond */ query = sqlite_mprintf(QUERY, cdr_table); if (!query) { ast_log(LOG_ERROR, "Unable to allocate SQL query\n"); unload_module(); return AST_MODULE_LOAD_DECLINE; } ast_debug(1, "SQL query: %s\n", query); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) sqlite_freemem(query); if (error) { /* * Unexpected error. */ if (error != SQLITE_ERROR) { ast_log(LOG_ERROR, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); unload_module(); return AST_MODULE_LOAD_DECLINE; } sqlite_freemem(errormsg); errormsg = NULL; query = sqlite_mprintf(sql_create_cdr_table, cdr_table); if (!query) { ast_log(LOG_ERROR, "Unable to allocate SQL query\n"); unload_module(); return AST_MODULE_LOAD_DECLINE; } ast_debug(1, "SQL query: %s\n", query); RES_CONFIG_SQLITE_BEGIN error = sqlite_exec(db, query, NULL, NULL, &errormsg); RES_CONFIG_SQLITE_END(error) sqlite_freemem(query); if (error) { ast_log(LOG_ERROR, "%s\n", S_OR(errormsg, sqlite_error_string(error))); sqlite_freemem(errormsg); unload_module(); return AST_MODULE_LOAD_DECLINE; } } sqlite_freemem(errormsg); errormsg = NULL; error = ast_cdr_register(RES_CONFIG_SQLITE_NAME, RES_CONFIG_SQLITE_DESCRIPTION, cdr_handler); if (error) { unload_module(); return AST_MODULE_LOAD_DECLINE; } cdr_registered = 1; } error = ast_cli_register_multiple(cli_status, ARRAY_LEN(cli_status)); if (error) { unload_module(); return AST_MODULE_LOAD_DECLINE; } cli_status_registered = 1; return AST_MODULE_LOAD_SUCCESS; } AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Realtime SQLite configuration", .support_level = AST_MODULE_SUPPORT_EXTENDED, .load = load_module, .unload = unload_module, .load_pri = AST_MODPRI_REALTIME_DRIVER, );