Added SQLite database support.

git-svn-id: http://yate.null.ro/svn/yate/trunk@5913 acf43c95-373e-0410-b603-e72c3f656dc1
This commit is contained in:
paulc 2014-09-16 13:04:05 +00:00
parent 9d5ead567d
commit ba13dedd67
5 changed files with 793 additions and 0 deletions

View File

@ -0,0 +1,87 @@
[general]
; This section is special - holds settings common to all connections
; priority: int: Handler priority for the "database" message
;priority=100
; shared_cache: bool: Use the SQLite shared cache by default
; Cache sharing can be enabled or disabled per database
;shared_cache=no
; Each other section in this file describes a database connection
;[default]
; The section name is used as the database connection name
; autostart: bool: Automatically initiate the connection on startup
;autostart=yes
; timeout: int: Query timeout in milliseconds
; Minimum value for timeout is 100
;timeout=2000
; retry: int: How many times to retry operations in case of contention
; Valid values are 0..100
;retry=5
; database: string: SQLite database filename or descriptor
; It can be a file name, a file: URI or a special value
; An empty string creates a non-sharable temporary file database
; The special value :memory: creates an in-memory non-sharable database
; Engine runtime parameters like ${configpath} will be substituted
;database=:memory:
; initialize: string: Semicolon separated SQLite queries to run after opening the database
; This can be used to apply various behavior changing PRAGMA or to populate a memory or
; temporary database (which is initially empty)
; If the first character is a @ then this setting indicates the name of a file holding the
; SQLite queries. Engine runtime parameters will be substituted in the file name.
;initialize=
; poolsize: int: Number of connections to establish for this account
; Pooling can be enabled only for shared cache databases
; Minimum number of connections is 1
;poolsize=1
[general]
; This section is special - holds settings common to all connections
; priority: int: Handler priority for the "database" message
;priority=100
; shared_cache: bool: Use the SQLite shared cache by default
; Cache sharing can be enabled or disabled per database
;shared_cache=no
; Each other section in this file describes a database connection
;[default]
; The section name is used as the database connection name
; autostart: bool: Automatically initiate the connection on startup
;autostart=yes
; timeout: int: Query timeout in milliseconds
; Minimum value for timeout is 100
;timeout=2000
; retry: int: How many times to retry operations in case of contention
; Valid values are 0..100
;retry=5
; database: string: SQLite database filename or descriptor
; It can be a file name, a file: URI or a special value
; An empty string creates a non-sharable temporary file database
; The special value :memory: creates an in-memory non-sharable database
;database=:memory:
; initialize: string: Semicolon separated SQLite queries to run after opening the database
; This can be used to apply various behavior changing PRAGMA or to populate a memory or
; temporary database (which is initially empty)
;initialize=
; poolsize: int: Number of connections to establish for this account
; Pooling can be enabled only for shared cache databases
; Minimum number of connections is 1
;poolsize=1

View File

@ -689,6 +689,36 @@ AC_SUBST(MYSQL_INC)
AC_SUBST(MYSQL_LIB)
AC_SUBST(MYSQL_VER)
HAVE_SQLITE=no
SQLITE_INC=""
SQLITE_LIB=""
AC_ARG_WITH(sqlite,AC_HELP_STRING([--with-sqlite=DIR],[use SQLite library from DIR]),[ac_cv_use_sqlite=$withval],[ac_cv_use_sqlite=yes])
if [[ "x$ac_cv_use_sqlite" != "xno" ]]; then
if [[ "x$ac_cv_use_sqlite" = "xyes" ]]; then
AC_MSG_CHECKING([for SQLite using pkg-config])
SQLITE_INC=`(pkg-config --cflags sqlite3) 2>/dev/null`
SQLITE_LIB=`(pkg-config --libs sqlite3) 2>/dev/null`
if [[ "x$SQLITE_INC$SQLITE_LIB" = "x" ]]; then
SQLITE_INC=""
SQLITE_LIB=""
else
HAVE_SQLITE=yes
fi
else
AC_MSG_CHECKING([for SQLite in $ac_cv_use_sqlite])
if [[ -f "$ac_cv_use_sqlite/sqlite3.h" ]]; then
SQLITE_INC="-I$ac_cv_use_sqlite"
SQLITE_LIB="-lsqlite3"
HAVE_SQLITE=yes
fi
fi
AC_MSG_RESULT([$HAVE_SQLITE])
fi
AC_SUBST(HAVE_SQLITE)
AC_SUBST(SQLITE_INC)
AC_SUBST(SQLITE_LIB)
HAVE_ZAP=no
ZAP_FLAGS=""
AC_ARG_ENABLE(dahdi,AC_HELP_STRING([--enable-dahdi],[Enable Dahdi driver (default: yes)]),want_dahdi=$enableval,want_dahdi=yes)

View File

@ -25,6 +25,9 @@ PGSQL_LIB := -lpq
HAVE_MYSQL := @HAVE_MYSQL@
MYSQL_INC := @MYSQL_INC@
MYSQL_LIB := @MYSQL_LIB@
HAVE_SQLITE := @HAVE_SQLITE@
SQLITE_INC := @SQLITE_INC@
SQLITE_LIB := @SQLITE_LIB@
HAVE_SPANDSP := @HAVE_SPANDSP@
SPANDSP_INC := @SPANDSP_INC@
SPANDSP_LIB := @SPANDSP_LIB@
@ -97,6 +100,10 @@ ifneq ($(HAVE_MYSQL),no)
PROGS := $(PROGS) server/mysqldb.yate
endif
ifneq ($(HAVE_SQLITE),no)
PROGS := $(PROGS) server/sqlitedb.yate
endif
ifneq (@HAVE_RESOLV@,no)
PROGS := $(PROGS) enumroute.yate
endif
@ -325,6 +332,9 @@ server/pgsqldb.yate: EXTERNLIBS = $(PGSQL_LIB)
server/mysqldb.yate: EXTERNFLAGS = $(MYSQL_INC)
server/mysqldb.yate: EXTERNLIBS = $(MYSQL_LIB)
server/sqlitedb.yate: EXTERNFLAGS = $(SQLITE_INC)
server/sqlitedb.yate: EXTERNLIBS = $(SQLITE_LIB)
client/alsachan.yate: EXTERNLIBS = -lasound
client/coreaudio.yate: EXTERNLIBS = -framework CoreServices -framework CoreAudio -framework AudioUnit -framework AudioToolbox

648
modules/server/sqlitedb.cpp Normal file
View File

@ -0,0 +1,648 @@
/**
* sqlitedb.cpp
* This file is part of the YATE Project http://YATE.null.ro
*
* This is the SQLite support from Yate.
*
* Yet Another Telephony Engine - a fully featured software PBX and IVR
* Copyright (C) 2014 Null Team
*
* This software is distributed under multiple licenses;
* see the COPYING file in the main directory for licensing
* information for this specific distribution.
*
* This use of this software may be subject to additional restrictions.
* See the LEGAL file in the main directory for details.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
#include <yatephone.h>
#include <stdio.h>
#include <sqlite3.h>
using namespace TelEngine;
namespace { // anonymous
class SqlConn;
static ObjList s_accounts;
Mutex s_conmutex(false,"SQLite::acc");
static unsigned int s_failedConns;
static bool s_sharedCache = false;
// Database account holding the connection(s)
class SqlAccount : public RefObject, public Mutex
{
friend class SqlConn;
public:
SqlAccount(const NamedList& sect);
// Try to initialize DB connections. Return true if at least one of them is active
bool initDb();
// Make a query
int queryDb(const char* query, Message* dest);
bool hasConn();
virtual const String& toString() const
{ return m_name; }
virtual void destroyed();
inline unsigned int total()
{ return m_totalQueries; }
inline unsigned int failed()
{ return m_failedQueries; }
inline unsigned int errorred()
{ return m_errorQueries; }
inline unsigned int queryTime()
{ return (unsigned int) m_queryTime; }
protected:
inline void incErrorQueriesSafe() {
Lock mylock(m_statsMutex);
m_errorQueries++;
}
private:
void dropDb();
String m_name;
String m_database;
String m_initialize;
int m_retry;
u_int64_t m_timeout;
SqlConn* m_connPool;
unsigned int m_connPoolSize;
// stat counters
Mutex* m_statsMutex;
unsigned int m_totalQueries;
unsigned int m_failedQueries;
unsigned int m_errorQueries;
u_int64_t m_queryTime;
};
// A database connection
class SqlConn : public String
{
friend class SqlAccount;
public:
SqlConn(SqlAccount* account = 0);
~SqlConn();
inline bool isBusy() const
{ return m_busy; }
inline void setBusy(bool busy)
{ m_busy = busy; }
inline int retries() const
{ return m_account ? m_account->m_retry : 5; }
// Test if the connection is still OK
inline bool testDb() const
{ return m_conn != 0; }
bool initDb();
void dropDb();
// Perform the query, fill the message with data, retry in case of errors
// Return number of rows, -1 for non-retryable errors and -2 for busy / timeout
int queryDb(const char* query, Message* dest);
virtual void destruct();
private:
SqlAccount* m_account;
bool m_busy;
sqlite3* m_conn;
};
class SqlModule : public Module
{
public:
SqlModule();
~SqlModule();
protected:
virtual void initialize();
virtual void statusModule(String& str);
virtual void statusParams(String& str);
virtual void statusDetail(String& str);
virtual void genUpdate(Message& msg);
private:
bool m_init;
};
static SqlModule module;
class SqlHandler : public MessageHandler
{
public:
SqlHandler(unsigned int prio = 100)
: MessageHandler("database",prio,module.name())
{ }
virtual bool received(Message& msg);
};
//
// SqlConn
//
SqlConn::SqlConn(SqlAccount* account)
: m_account(account), m_busy(false),
m_conn(0)
{
}
SqlConn::~SqlConn()
{
dropDb();
}
// Initialize the database connection and handler data
bool SqlConn::initDb()
{
if (testDb())
return true;
dropDb();
Debug(&module,DebugAll,"'%s' opening database \"%s\" [%p]",
c_str(),m_account->m_database.safe(),m_account);
if (sqlite3_open(m_account->m_database.safe(),&m_conn) != SQLITE_OK) {
Debug(&module,DebugWarn,"Failed to open database '%s': %s",
c_str(),sqlite3_errmsg(m_conn));
dropDb();
return false;
}
return true;
}
// Drop the connection
void SqlConn::dropDb()
{
if (!m_conn)
return;
sqlite3* tmp = m_conn;
m_conn = 0;
XDebug(&module,DebugAll,"Database '%s' dropped [%p]",c_str(),m_account);
if (sqlite3_close(tmp) != SQLITE_OK)
Debug(&module,DebugWarn,"Failed to close database '%s': %s",
c_str(),sqlite3_errmsg(tmp));
}
// Perform the query, fill the message with data, retry in case of errors
// Return number of rows, -1 for non-retryable errors and -2 for busy / timeout
int SqlConn::queryDb(const char* query, Message* dest)
{
if (!initDb())
// no retry - initDb already tried and failed...
return -1;
bool results = dest && dest->getBoolValue("results",true);
int changed = sqlite3_total_changes(m_conn);
int rows = 0;
int cols = -1;
while (query) {
while (';' == *query || ' ' == *query || '\t' == *query || '\r' == *query || '\n' == *query)
query++;
if (!*query)
break;
int retry = retries();
sqlite3_stmt* stmt;
const char* tail;
int i;
// Prepare statement, leave whatever unparsed in tail
for (i = 0; i >= 0; i++) {
if (i)
Thread::idle();
stmt = 0;
tail = 0;
switch (sqlite3_prepare_v2(m_conn,query,-1,&stmt,&tail)) {
case SQLITE_OK:
i = -2;
break;
case SQLITE_BUSY:
case SQLITE_LOCKED:
sqlite3_finalize(stmt);
if (i >= retry)
return -2;
continue;
default:
{
const char* errStr = sqlite3_errmsg(m_conn);
Debug(&module,DebugWarn,"Query '%s' for '%s' prepare error: %s [%p]",
query,c_str(),errStr,m_account);
if (dest)
dest->setParam("error",errStr);
}
sqlite3_finalize(stmt);
if (results)
dest->userData(0);
return -1;
}
}
int lr = 0;
int lc = 0;
Array* a = 0;
// Execute statement, collect results if needed
for (i = 0; i >= 0; ) {
if (i)
Thread::idle();
switch (sqlite3_step(stmt)) {
case SQLITE_DONE:
if (lr || !rows) {
rows = lr;
cols = lc;
if (results) {
dest->userData(a);
a = 0;
}
}
i = -2;
break;
case SQLITE_ROW:
if (!lr++)
lc = sqlite3_column_count(stmt);
if (!results)
continue;
if (!a) {
a = new Array(lc,2);
for (int j = 0; j < lc; j++)
a->set(new String(sqlite3_column_name(stmt,j)),j,0);
}
else
a->addRow();
for (int j = 0; j < lc; j++) {
GenObject* v = 0;
switch (sqlite3_column_type(stmt,j)) {
case SQLITE_NULL:
break;
case SQLITE_BLOB:
{
// Must do this in two steps to guarantee call order
void* data = const_cast<void*>(sqlite3_column_blob(stmt,j));
v = new DataBlock(data,sqlite3_column_bytes(stmt,j));
}
break;
default:
v = new String(reinterpret_cast<const char*>(sqlite3_column_text(stmt,j)));
}
a->set(v,j,lr);
}
continue;
case SQLITE_BUSY:
case SQLITE_LOCKED:
if (i++ >= retry) {
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
TelEngine::destruct(a);
if (results)
dest->userData(0);
return -2;
}
continue;
default:
{
const char* errStr = sqlite3_errmsg(m_conn);
Debug(&module,DebugWarn,"Query '%s' for '%s' execute error: %s [%p]",
query,c_str(),errStr,m_account);
if (dest)
dest->setParam("error",errStr);
}
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
TelEngine::destruct(a);
if (results)
dest->userData(0);
m_account->incErrorQueriesSafe();
return -1;
}
}
// Clean up statement and advance to next one
sqlite3_reset(stmt);
sqlite3_finalize(stmt);
TelEngine::destruct(a);
query = tail;
}
changed = sqlite3_total_changes(m_conn) - changed;
if (dest) {
dest->setParam("rows",String(rows));
if (cols >= 0)
dest->setParam("columns",String(cols));
dest->setParam("affected",String(changed));
}
return rows;
}
void SqlConn::destruct()
{
dropDb();
String::destruct();
}
//
// SqlAccount
//
SqlAccount::SqlAccount(const NamedList& sect)
: Mutex(true,"SqlAccount"),
m_name(sect),
m_connPool(0), m_connPoolSize(0),
m_statsMutex(&s_conmutex),
m_totalQueries(0), m_failedQueries(0),
m_errorQueries(0), m_queryTime(0)
{
m_database = sect.getValue("database",":memory:");
Engine::runParams().replaceParams(m_database);
m_initialize = sect.getValue("initialize");
if (m_initialize.startSkip("@",false)) {
Engine::runParams().replaceParams(m_initialize);
m_initialize.trimBlanks();
File f;
if (f.openPath(m_initialize)) {
int64_t len = f.length();
if (len > 0 && len <= 65536) {
DataBlock d(0,len + 1);
if (f.readData(d.data(),len) == len)
m_initialize = (const char*)d.data();
else {
Debug(&module,DebugWarn,"Failed to read init file '%s'",m_initialize.c_str());
m_initialize.clear();
}
}
else {
Debug(&module,DebugWarn,"Empty or too long file '%s'",m_initialize.c_str());
m_initialize.clear();
}
}
else {
Debug(&module,DebugWarn,"Missing init file '%s'",m_initialize.c_str());
m_initialize.clear();
}
}
m_timeout = (u_int64_t)1000 * sect.getIntValue("timeout",2000);
if (m_timeout < 100000)
m_timeout = 100000;
m_retry = sect.getIntValue("retry",5,0,100,false);
// Can create just one connection to temporary or non shared cache in-memory databases
bool shared = s_sharedCache && !m_database.null();
shared = shared && (m_database.find(":memory:") < 0) && (m_database.find("mode=memory") < 0);
shared = shared || (m_database.startsWith("file:") && (m_database.find("cache=shared") >= 0));
m_connPoolSize = sect.getIntValue("poolsize",1,1);
if ((m_connPoolSize > 1) && !shared) {
Debug(&module,DebugConf,"Disabling pooling for non shared cache account '%s'",
m_name.c_str());
m_connPoolSize = 1;
}
m_connPool = new SqlConn[m_connPoolSize];
for (unsigned int i = 0; i < m_connPoolSize; i++) {
m_connPool[i].m_account = this;
m_connPool[i].assign(m_name + "." + String(i + 1));
}
Debug(&module,DebugInfo,"Database account '%s' created poolsize=%u [%p]",
m_name.c_str(),m_connPoolSize,this);
}
// Init the connections for the account, run init query
bool SqlAccount::initDb()
{
bool ok = false;
for (unsigned int i = 0; i < m_connPoolSize; i++) {
ok = m_connPool[i].initDb() || ok;
if (ok && m_initialize && (i == 0) && (m_connPool[i].queryDb(m_initialize,0) < 0))
Debug(&module,DebugWarn,"Failed to run initializer for account '%s'",m_name.c_str());
}
return ok;
}
void SqlAccount::destroyed()
{
s_conmutex.lock();
s_accounts.remove(this,false);
s_conmutex.unlock();
dropDb();
if (m_connPool)
delete[] m_connPool;
m_connPoolSize = 0;
Debug(&module,DebugInfo,"Database account '%s' destroyed [%p]",m_name.c_str(),this);
}
// drop the connection
void SqlAccount::dropDb()
{
for (unsigned int i = 0; i < m_connPoolSize; i++)
m_connPool[i].dropDb();
}
static bool failure(Message* m)
{
if (m)
m->setParam("error","failure");
return false;
}
int SqlAccount::queryDb(const char* query, Message* dest)
{
if (TelEngine::null(query))
return -1;
Debug(&module,DebugAll,"Performing query \"%s\" for '%s'",
query,m_name.c_str());
// Use a while() to break to the end to update statistics
int res = -1;
u_int64_t start = Time::now();
while (true) {
Lock mylock(this,(long)m_timeout);
if (!mylock.locked()) {
Debug(&module,DebugWarn,"Failed to lock '%s' for " FMT64U " usec",
m_name.c_str(),m_timeout);
break;
}
// Find a non busy connection
SqlConn* conn = 0;
SqlConn* notConnected = 0;
for (unsigned int i = 0; i < m_connPoolSize; i++) {
if (m_connPool[i].isBusy())
continue;
if (m_connPool[i].testDb()) {
conn = &(m_connPool[i]);
break;
}
if (!notConnected)
notConnected = &(m_connPool[i]);
}
if (!conn)
conn = notConnected;
if (!conn) {
// Wait for a connection to become non-busy
// Round up the number of intervals to wait
unsigned int n = (unsigned int)((m_timeout + 999999) / Thread::idleUsec());
for (unsigned int i = 0; i < n; i++) {
for (unsigned int j = 0; j < m_connPoolSize; j++) {
if (!m_connPool[j].isBusy() && m_connPool[j].testDb()) {
conn = &(m_connPool[j]);
break;
}
}
if (conn || Thread::check(false))
break;
Thread::idle();
}
}
if (conn)
conn->setBusy(true);
else
Debug(&module,DebugWarn,"Account '%s' failed to pick a connection [%p]",m_name.c_str(),this);
mylock.drop();
if (conn) {
res = conn->queryDb(query,dest);
conn->setBusy(false);
}
break;
}
Lock stats(m_statsMutex);
m_totalQueries++;
if (res > -2) {
if (res < 0)
m_failedQueries++;
u_int64_t finish = Time::now() - start;
m_queryTime += finish;
}
stats.drop();
module.changed();
if (res < 0)
failure(dest);
return res;
}
bool SqlAccount::hasConn()
{
for (unsigned int i = 0; i < m_connPoolSize; i++)
if (m_connPool[i].testDb())
return true;
return false;
}
static SqlAccount* findDb(const String& account)
{
if (account.null())
return 0;
return static_cast<SqlAccount*>(s_accounts[account]);
}
bool SqlHandler::received(Message& msg)
{
const String* str = msg.getParam("account");
if (TelEngine::null(str))
return false;
s_conmutex.lock();
RefPointer<SqlAccount> db = findDb(*str);
s_conmutex.unlock();
if (!db)
return false;
str = msg.getParam("query");
if (!TelEngine::null(str))
db->queryDb(*str,&msg);
db = 0;
msg.setParam("dbtype","sqlitedb");
return true;
}
SqlModule::SqlModule()
: Module ("sqlitedb","database",true),m_init(false)
{
String tmp(sqlite3_libversion());
if (tmp != SQLITE_VERSION)
Debug(this,DebugConf,"SQLite version mismatch: expecting %s but library is %s",
SQLITE_VERSION,tmp.c_str());
Output("Loaded module SQLite based on %s",SQLITE_VERSION);
}
SqlModule::~SqlModule()
{
Output("Unloading module SQLite");
s_accounts.clear();
if (m_init) {
sqlite3_shutdown();
m_init = false;
}
}
void SqlModule::statusModule(String& str)
{
Module::statusModule(str);
str.append("format=Total|Failed|Errors|AvgExecTime",",");
}
void SqlModule::statusParams(String& str)
{
s_conmutex.lock();
str.append("conns=",",") << s_accounts.count();
str.append("failed=",",") << s_failedConns;
s_conmutex.unlock();
}
void SqlModule::statusDetail(String& str)
{
s_conmutex.lock();
for (ObjList* o = s_accounts.skipNull(); o; o = o->skipNext()) {
SqlAccount* acc = static_cast<SqlAccount*>(o->get());
str.append(acc->toString().c_str(),",") << "=" << acc->total() << "|" << acc->failed()
<< "|" << acc->errorred() << "|";
if (acc->total() - acc->failed() > 0)
str << (acc->queryTime() / (acc->total() - acc->failed()) / 1000); //miliseconds
else
str << "0";
}
s_conmutex.unlock();
}
void SqlModule::initialize()
{
Module::initialize();
if (m_init)
return;
Output("Initializing module SQLite");
Configuration cfg(Engine::configFile("sqlitedb"));
s_sharedCache = cfg.getBoolValue("general","shared_cache",false);
int err = sqlite3_initialize();
if (err != SQLITE_OK) {
Alarm(this,DebugWarn,"SQLite initialize failed, code %d",err);
return;
}
sqlite3_enable_shared_cache(s_sharedCache);
unsigned int i;
for (i = 0; i < cfg.sections(); i++) {
NamedList* sec = cfg.getSection(i);
if (!sec || (*sec == "general"))
continue;
SqlAccount* acc = new SqlAccount(*sec);
if (sec->getBoolValue("autostart",true) && !acc->initDb())
TelEngine::destruct(acc);
s_conmutex.lock();
if (acc) {
s_accounts.insert(acc);
m_init = true;
}
else
s_failedConns++;
s_conmutex.unlock();
}
if (m_init)
Engine::install(new SqlHandler(cfg.getIntValue("general","priority",100)));
else
sqlite3_shutdown();
}
void SqlModule::genUpdate(Message& msg)
{
unsigned int index = 0;
s_conmutex.lock();
for (ObjList* o = s_accounts.skipNull(); o; o = o->skipNext()) {
SqlAccount* acc = static_cast<SqlAccount*>(o->get());
msg.setParam(String("database.") << index,acc->toString());
msg.setParam(String("total.") << index,String(acc->total()));
msg.setParam(String("failed.") << index,String(acc->failed()));
msg.setParam(String("errorred.") << index,String(acc->errorred()));
msg.setParam(String("hasconn.") << index,String::boolText(acc->hasConn()));
msg.setParam(String("querytime.") << index,String(acc->queryTime()));
index++;
}
s_conmutex.unlock();
msg.setParam("count",String(index));
}
}; // anonymous namespace
/* vi: set ts=8 sw=4 sts=4 noet: */

View File

@ -428,6 +428,21 @@ that support database access will be able to use MySQL.
%config(noreplace) %{_sysconfdir}/yate/mysqldb.conf
%package sqlite
Summary: SQLite database driver for Yate
Group: Applications/Communication
Requires: %{name} = %{version}-%{release}
Provides: %{name}-database
%description sqlite
This package allows Yate to use SQLite database files. All modules
that support database access will be able to use SQLite.
%files sqlite
%{_libdir}/yate/server/sqlitedb.yate
%config(noreplace) %{_sysconfdir}/yate/sqlitedb.conf
%if "%{no_gui}" != "1"
%package client-common
@ -622,6 +637,9 @@ rm -rf %{buildroot}
%changelog
* Tue Apr 29 2014 Paul Chitescu <paulc@voip.null.ro>
- Added SQLite module and subpackage
* Mon Apr 23 2012 Paul Chitescu <paulc@voip.null.ro>
- Added new support module gvoice