Chapter 16. Relational Database Integration
Few things are harder to put up with than the annoyance of a good example.
In this chapter we are going to explore integrating some Asterisk features and functions into a database. There are several databases available for Linux, but we have chosen to limit our discussion to the two most popular: PostgreSQL and MySQL.
We will also explain how to configure Linux to connect to a Microsoft SQL database via ODBC; however, configuration of the Windows/Microsoft portion is beyond the scope of this book.
Regardless of which database you use, this chapter focuses primarily on the ODBC connector, so as long as you have some familiarity with getting your favorite database ODBC-ready, you shouldn’t have any problems with this chapter.
Integrating Asterisk with databases is one of
the fundamental aspects of building a large clustered or distributed system.
The power of the database will enable you to use dynamically changing data
in your dialplans, for tasks such as sharing information across an array of
Asterisk systems or integrating with web-based services. Our favorite
dialplan function, which we will cover later in
this chapter, is func_odbc.
While not all Asterisk deployments will require relational databases, understanding how to harness them opens a treasure chest full of new ways to design your telecom solution.
Installing and Configuring PostgreSQL and MySQL
In the following sections we will show how to install and configure PostgreSQL and MySQL on both CentOS and Ubuntu.[139] It is recommended that you only install one database at a time while working through this section. Pick the database you are most comfortable with, as there is no wrong choice.
Installing PostgreSQL for CentOS
The following command can be used to install the PostgreSQL server and its dependencies from the console:
Install 3 Package(s) Upgrade 0 Package(s) Total download size: 6.9 M Is this ok [y/N]:$sudo yum install -y postgresql-servery
Then start the database, which will take a few seconds to initialize for the first time:
$sudo service postgresql start
Now head to the section called “Configuring PostgreSQL” for instructions on how to perform the initial configuration.
Installing PostgreSQL for Ubuntu
To install PostgreSQL on Ubuntu, run the following command. You will be prompted to also install any additional packages that are dependencies of the application. Press Enter to accept the list of dependencies, at which point the packages will be installed and PostgreSQL will be automatically started and initialized:
$sudo apt-get install postgresql...After this operation, 19.1MB of additional disk space will be used. Do you want to continue [Y/n]?y
Now head to the section called “Configuring PostgreSQL” for instructions on how to perform the initial configuration.
Installing MySQL for CentOS
To install MySQL on CentOS, run the following command. You will be prompted to install several dependencies. Press Enter to accept, and the MySQL server and dependency packages will be installed:
$sudo yum install mysql-serverInstall 5 Package(s) Upgrade 0 Package(s) Total download size: 27 M Is this ok [y/N]:y
Then start the MySQL database by running:
$sudo service mysqld start
Now head to the section called “Configuring MySQL” to perform the initial configuration.
Installing MySQL for Ubuntu
To install MySQL on Ubuntu, run the following command. You will be prompted to install several dependencies. Press Enter to accept, and the MySQL server and its dependency packages will be installed:
$sudo apt-get install mysql-serverNeed to get 24.0MB of archives. After this operation, 60.6MB of additional disk space will be used. Do you want to continue [Y/n]?y
During the installation, you will be placed into a configuration wizard to help you through the initial configuration of the database. You will be prompted to enter a new password for the root user. Type in a strong password and press Enter. You will then be asked to confirm the password. Type your strong password again, followed by Enter. You will then be returned to the console, where the installation will complete. The MySQL service will now be running.
Now head to the section called “Configuring MySQL” to perform the initial configuration.
Configuring PostgreSQL
Next, create a user called asterisk, which you will use to connect to and manage the database. You can switch to the postgres user by using the following command:
$sudo su - postgres
Note
At the time of this writing, PostgreSQL version 8.1.x is utilized on CentOS, and 8.4.x on Ubuntu.
Then run the following commands to create the asterisk user in the database and set up permissions:
$createuser -PEnter name of user to add:asteriskEnter password for new user:Enter it again:Shall the new role be a superuser? (y/n)nShall the new user be allowed to create databases? (y/n)yShall the new user be allowed to create more new users? (y/n)nCREATE ROLE
Now, edit the pg_hba.conf file in order to allow the
asterisk user you just created to connect to the
PostgreSQL server over the TCP/IP socket.
On CentOS, this file will be located at /var/lib/pgsql/data/pg_hba.conf. On Ubuntu,
you will find it at /etc/postgresql/8.4/main/pg_hba.conf.
At the end of the file, replace everything below this line:
# TYPE DATABASE USER CIDR-ADDRESS METHOD
with the following:
# TYPE DATABASE USER CIDR-ADDRESS METHOD host all asterisk 127.0.0.1/32 md5 local all asterisk trust
Now you can create the database that we
will use throughout this chapter. Call the database asterisk and set the owner to your
asterisk user:
$createdb --owner=asterisk asteriskCREATE DATABASE
You can set the password for the asterisk user like so:
$psql -d template1template1=#"ALTER USER asterisk WITH PASSWORD 'template1=#password'"\q
Exit from the postgres user:
$exit
Then restart the PostgreSQL server. On CentOS:
$sudo service postgresql restart
Note
You need to restart the PostgreSQL
service because you made changes to pg_hba.conf, not because you added a new
user or changed the password.
On Ubuntu:
$sudo /etc/init.d/postgresql-8.4 restart
Note
On Ubuntu 10.10 and newer the version number seems to be dropped, so it may just be /etc/init.d/postgresql restart.
You can verify your connection to the PostgreSQL server via TCP/IP, like so:
$psql -h 127.0.0.1 -U asteriskPassword for user asterisk: Welcome to psql 8.1.21, the PostgreSQL interactive terminal. Type: \copyright for distribution terms \h for help with SQL commands \? for help with psql commands \g or terminate with semicolon to execute query \q to quitasterisk=>
You’re now ready to move on to the section called “Installing and Configuring ODBC”.
Configuring MySQL
With the MySQL database now running, you should secure your installation. Conveniently, there is a script you can execute that will allow you to enter a new password[140] for the root user, along with some additional options. The script is pretty straightforward, and after entering and confirming your root password you can continue to select the defaults unless you have a specific reason not to.
Execute the following script:
$sudo /usr/bin/mysql_secure_installation
Then connect to the database console so you can create your asterisk user and set up permissions:
$mysql -u root -pEnter password:
After
entering the password, you will be presented with the mysql console prompt. You can now create your
asterisk user by executing the CREATE USER command. The % is a wildcard indicating the
asterisk user can connect from any host and is
IDENTIFIED BY the password
some_secret_password (which you should
obviously change). Note the trailing semicolon:
mysql>CREATE USER 'asterisk'@'%' IDENTIFIED BY 'some_secret_password';Query OK, 0 rows affected (0.00 sec)
Let’s also create the initial database you’ll use throughout this chapter:
mysql>CREATE DATABASE asterisk;Query OK, 1 rows affected (0.00 sec)
Now
that you’ve created your user and database, you need to assign
permissions for the asterisk user
to access the asterisk
database:
mysql>GRANT ALL PRIVILEGES ON asterisk.* TO 'asterisk'@'%';Query OK, 0 rows affected (0.00 sec)
Finally, exit from the console and verify that your permissions are correct by logging back into the asterisk database as the asterisk user:
mysql>exitBye#mysql -u asterisk -p asteriskEnter password:mysql>
You’re now ready to move on to the section called “Installing and Configuring ODBC”.
Installing and Configuring ODBC
The ODBC connector is a database abstraction layer that makes it possible for Asterisk to communicate with a wide range of databases without requiring the developers to create a separate database connector for every database Asterisk wants to support. This saves a lot of development effort and code maintenance. There is a slight performance cost, because we are adding another application layer between Asterisk and the database, but this can be mitigated with proper design and is well worth it when you need powerful, flexible database capabilities in your Asterisk system.
Before you install the connector in Asterisk, you have to install ODBC into Linux itself. To install the ODBC drivers, use one of the following commands.
On CentOS:
$sudo yum install unixODBC unixODBC-devel libtool-ltdl libtool-ltdl-devel
Note
If you’re using a 64-bit installation,
remember to add .x86_64 to the
end of your development packages to make sure the i386 packages are
not also installed, as stability problems can result if Asterisk links
against the wrong libraries.
On Ubuntu:
$sudo apt-get install unixODBC unixODBC-dev
Tip
See Chapter 3, Installing Asterisk for the matrix of packages you should have installed.
You’ll also need to install the unixODBC development package, because Asterisk uses it to build the ODBC modules we will be using throughout this chapter.
Warning
The unixODBC drivers
shipped with distributions are often a few versions behind the
officially released versions on the http://www.unixodbc.org website. If you have stability
issues while using unixODBC, you may need to
install from source. Just be sure to remove the
unixODBC drivers via your package manager first,
and then update the paths in your /etc/odbcinst.ini file.
By default, CentOS will install the drivers for connecting to PostgreSQL databases via ODBC. To install the drivers for MySQL, execute the following command:
$sudo yum install mysql-connector-odbc
To install the PostgreSQL ODBC connector on Ubuntu:
$sudo apt-get install odbc-postgresql
Or to install the MySQL ODBC connector on Ubuntu:
$sudo apt-get install libmyodbc
Configuring ODBC for PostgreSQL
Configuration for the PostgreSQL ODBC driver is done in
the /etc/odbcinst.ini file.
On CentOS the default file already contains some data, including that for PostgreSQL, so just verify that the data exists. The file will look like the following:
[PostgreSQL] Description = ODBC for PostgreSQL Driver = /usr/lib/libodbcpsql.so Setup = /usr/lib/libodbcpsqlS.so FileUsage = 1
On Ubuntu, the /etc/odbcinst.ini file will be blank, so
you’ll need to add the data to that configuration file. Add the
following to the odbcinst.ini
file:
[PostgreSQL] Description = ODBC for PostgreSQL Driver = /usr/lib/odbc/psqlodbca.so Setup = /usr/lib/odbc/libodbcpsqlS.so FileUsage = 1
Note
On 64-bit systems, you will need to
change the path of the libraries from /usr/lib/ to /usr/lib64/ in order to access the
correct library files.
In either case, you can use cat > /etc/odbcinst.ini to write a clean configuration file, as we’ve done in other chapters. Just use Ctrl+D to save the file once you’re done.
Verify that the system is able to see the
driver by running the following command. It should return the label name
PostgreSQL if all is well:
$odbcinst -q -d[PostgreSQL]
Next, configure the /etc/odbc.ini file, which is used to create
an identifier that Asterisk will use to reference this configuration. If
at any point in the future you need to change the database to something
else, you simply need to reconfigure this file, allowing Asterisk to continue to point to the same
place[141]:
[asterisk-connector]
Description = PostgreSQL connection to 'asterisk' database
Driver = PostgreSQL
Database = asterisk
Servername = localhost
UserName = asterisk
Password = welcome
Port = 5432
Protocol = 8.1
ReadOnly = No
RowVersioning = No
ShowSystemTables = No
ShowOidColumn = No
FakeOidIndex = No
ConnSettings =
Configuring ODBC for MySQL
Configuration for the MySQL ODBC driver is done in the
/etc/odbcinst.ini file.
On CentOS the default file already contains some data, including that for MySQL, but it needs to be uncommented and requires a couple of changes. Replace the existing text with the following:
[MySQL] Description = ODBC for MySQL Driver = /usr/lib/libmyodbc3.so Setup = /usr/lib/libodbcmyS.so FileUsage = 1
On Ubuntu, the /etc/odbcinst.ini file will be blank, so
you’ll need to add the data to that configuration file. Add the
following to the odbcinst.ini
file:
[MySQL] Description = ODBC for MySQL Driver = /usr/lib/odbc/libmyodbc.so Setup = /usr/lib/odbc/libodbcmyS.so FileUsage = 1
Note
On 64-bit systems, you will need to
change the path of the libraries from /usr/lib/ to /usr/lib64/ in order to access the
correct library files.
In either case, you can use cat > /etc/odbcinst.ini to write a clean configuration file, as we’ve done in other chapters. Just use Ctrl+D to save the file once you’re done.
Verify that the system is able to see the
driver by running the following command. It should return the label name
MySQL if all is well:
#odbcinst -q -d[MySQL]
Next, configure the /etc/odbc.ini file, which is used to create
an identifier that Asterisk will use to reference this configuration. If
at any point in the future you need to change the database to something
else, you simply need to reconfigure this file, allowing Asterisk to continue to point to the same
place:
[asterisk-connector]
Description = MySQL connection to 'asterisk' database
Driver = MySQL
Database = asterisk
Server = localhost
UserName = asterisk
Password = welcome
Port = 3306
Socket = /var/lib/mysql/mysql.sock
Note
On Ubuntu 10.10, the socket location is
/var/run/mysqld/mysqld.sock.
Configuring ODBC for Microsoft SQL
Connecting to Microsoft SQL (MS SQL) is similar to connecting to either MySQL or PostgreSQL, as we’ve previously discussed. The configuration of MS SQL is beyond the scope of this book, but the following information will get your Asterisk box configured to connect to your MS SQL database once you’ve enabled the appropriate permissions on your database.
To connect to MS SQL, you need to install the FreeTDS drivers using the package manager (or by compiling via the source files available at http://www.freetds.org).
On CentOS:
$sudo yum install freetds
$sudo apt-get install freetds
After
installing the drivers, you need to configure the /etc/odbcinst.ini file, which tells the
system where the driver files are located.
Insert the following text into the
/etc/odbcinst.ini file with your
favorite text editor or with the following command:
$sudo cat > /etc/odbcinst.ini[FreeTDS] Description = ODBC for Microsoft SQL Driver = /usr/lib/libtdsodbc.so UsageCount = 1 Threading = 2Ctrl+D
Note
If you compiled via source, the files
may be located in /usr/local/lib/ or (if you compiled on a
64-bit system) /usr/local/lib64/.
Verify that the system is able to see the
driver by running the following command. It should return the label name
FreeTDS if all is well:
$odbcinst -q -d[FreeTDS]
Once you’ve configured the drivers, you need to modify the
/etc/odbc.ini file to control how
to connect to the database:
[asterisk-connector]
Description = MS SQL connection to 'asterisk' database
Driver = FreeTDS
Database = asterisk
Server = 192.168.100.1
UserName = asterisk
Password = welcome
Trace = No
TDS_Version = 7.0
Port = 1433In the next section, you will be able to validate your connection to the MS SQL server.
Validating the ODBC Connector
Now, verify that you can connect to your database using
the isql application. echo the select
1 statement and pipe it into isql, which will then connect using the
asterisk-connector section you added
to /etc/odbc.ini. You should get
the following output (or at least something similar; we’re looking for a
result of 1 rows fetched):
$echo "select 1" | isql -v asterisk-connector+---------------------------------------+ | Connected! | | | | sql-statement | | help [tablename] | | quit | | | +---------------------------------------+ SQL> +------------+ | ?column? | +------------+ | 1 | +------------+ SQLRowCount returns 1 1 rows fetched$exit
With
unixODBC installed, configured, and verified to
work, you need to recompile Asterisk so that the ODBC modules are
created and installed. Change back to your Asterisk source directory and
run the ./configure script so it
knows you have installed unixODBC:
$cd ~/src/asterisk-complete/asterisk/1.8$./configure$make menuselect$make install
Note
Almost everything in this chapter is
turned on by default. You will want to run make menuselect to verify that the ODBC-related modules are enabled. These
include cdr_odbc, cdr_adaptive_odbc, func_odbc, func_realtime, pbx_realtime, res_config_odbc, and res_odbc. For voicemail stored in an ODBC
database, be sure to select ODBC_STORAGE from the Voicemail
Build Options menu. You can verify that the modules exist
in the /usr/lib/asterisk/modules/
directory.
Configuring res_odbc to Allow Asterisk to Connect Through ODBC
Asterisk ODBC connections are configured in the res_odbc.conf file located in /etc/asterisk. The res_odbc.conf file sets the parameters that
various Asterisk modules will use to connect to the database.
Note
The pooling and limit options are quite useful for MS SQL and Sybase
databases. These permit you to establish multiple connections (up to
limit connections) to a database
while ensuring that each connection has only one statement executing
at once (this is due to a limitation in the protocol used by these
database servers).
Modify the res_odbc.conf file so it looks like the
following:
[asterisk] enabled => yes dsn => asterisk-connector username => asterisk password => welcome pooling => no limit => 1 pre-connect => yes
The dsn option points at the database connection
you configured in /etc/odbc.ini,
and the pre-connect option tells
Asterisk to open up and maintain a connection to the database when
loading the res_odbc.so module.
This lowers some of the overhead that would come from repeatedly setting
up and tearing down the connection to the database.
Once you’ve configured res_odbc.conf, start Asterisk and verify the
database connection with the odbc
show CLI command:
*CLI>odbc showODBC DSN Settings ----------------- Name: asterisk DSN: asterisk-connector Last connection attempt: 1969-12-31 19:00:00 Pooled: No Connected: Yes
Managing Databases
While it isn’t within the scope of this book to teach you about how to manage your databases, it is worth at least noting briefly some of the applications you could use to help with database management. Several exist, some of which are local client applications running from your computer and connecting to the database, and others of which are web-based applications that could be served from the same computer running the database itself, thereby allowing you to connect remotely.
Some of the ones we’ve used include:
phpMyAdmin (http://www.phpmyadmin.net)
MySQL Workbench (http://wb.mysql.com)
pgAdmin (http://www.pgadmin.org)
Navicat (commercial) (http://www.navicat.com)
Troubleshooting Database Issues
When working with ODBC database connections and Asterisk,
it is important to remember that the ODBC connection abstracts some of
the information passed between Asterisk and the database. In cases where
things are not working as expected, you may need to enable logging on
your database platform to see what Asterisk is sending to the database
(e.g., what SELECT, INSERT, or UPDATE statements are being triggered from
Asterisk), what the database is
seeing, and why the database may be rejecting the statements.
For example, one of the most common
problems found with ODBC database integration is an incorrectly defined
table, or a missing column that Asterisk expects to exist. While great
strides have been made in the form of adaptive modules, not all parts of
Asterisk are adaptive. In the case of ODBC voicemail storage, you may
have missed a column such as flag,
which is a new column not previously found in versions of Asterisk prior
to 1.8.[142] In order to debug why your data is not being written to
the database as expected, you should enable statement logging on the
database side, and then determine what statement is being executed and
why the database is rejecting it.
A Gentle Introduction to func_odbc
The very first use of func_odbc, which occurred while its author was
still writing it, is also a good introduction to its use. A customer of
one of the module’s authors noted that some people calling into his switch
had figured out a way to make free calls with his system. While his
eventual intent was to change his dialplan to avoid those problems, he
needed to blacklist certain caller IDs in the meantime, and the database
he wanted to use for this was a Microsoft SQL Server database. With a few
exceptions, this is the actual dialplan[143]:
[span3pri]
exten => _50054XX,1,NoOp()
same => n,Set(CDR(accountcode)=pricall)
same => n,GotoIf($[${ODBC_ANIBLOCK(${CALLERID(number)})}]?busy)
same => n(dial),Dial(DAHDI/G1/${EXTEN})
same => n(busy),Busy(10)
same => n,Hangup
This dialplan, in a nutshell, passes through all calls to
another system for routing purposes, except those calls whose caller IDs
are in a blacklist. The calls coming into this system used a block of 100
7-digit DIDs. There is a mystery function in this dialplan, though:
ODBC_ANIBLOCK(). This function is
defined in another configuration file, func_odbc.conf, at runtime:
[ANIBLOCK]
dsn=telesys
readsql=SELECT IF(COUNT(1)>0, 1, 0) FROM Aniblock WHERE NUMBER='${ARG1}'So,
your ODBC_ANIBLOCK()[144] connects to a listing in res_odbc.conf named telesys
and selects a count of records that have the NUMBER specified by the argument, which is,
referring to our dialplan above, the caller ID. Nominally, this function
should return either a 1 (indicating
the caller ID exists in the Aniblock
table) or a 0 (if it does not). This
value also evaluates directly to true or false, which means we don’t need
to use an expression in our dialplan to complicate the logic.
Getting Funky with func_odbc: Hot-Desking
The func_odbc dialplan
function is arguably the coolest and most powerful dialplan function in
Asterisk. It allows you to create and use fairly simple dialplan functions
that retrieve and use information from databases directly in the dialplan.
There are all kinds of ways in which this might be used, such as for
managing users or allowing the sharing of dynamic information within a
clustered set of Asterisk machines.
What func_odbc allows you to do is define SQL queries
to which you assign function names. In effect, you are creating custom
functions that obtain their results by executing queries against a
database. The func_odbc.conf file is
where you specify the relationships between the function names you create
and the SQL statements you wish them to perform. By referring to the named
functions in the dialplan, you can retrieve and update values in the
database.
Tip
While using an external script to interact with a database (from which a flat file is created that Asterisk will read) has advantages (if the database goes down, your system will continue to function and the script will simply not update any files until connectivity to the database is restored), it also has disadvantages. A major disadvantage is that any changes you make to a user will not be available until you run the update script. This is probably not a big issue on small systems, but on large systems waiting for changes to take effect can cause issues, such as pausing a live call while a large file is loaded and parsed.
You can relieve some of this by utilizing a replicated database system. Asterisk 1.6.0 and newer provide the ability to fail over to another database system. This way, you can cluster the database backend utilizing a master-master relationship (for PostgreSQL, pgcluster or Postgres-R;[145] for MySQL it’s native[146]), or a master-slave (for PostgreSQL, Slony-I, for MySQL it’s native) replication system.
In order to get you into the right frame of mind for what follows, we want you to picture a Dagwood sandwich.[147]
Can you relay the total experience of such a
thing by showing someone a picture of a tomato, or by waving a slice of
cheese about? Not hardly. That is the conundrum we faced when trying to
give a useful example of why func_odbc
is so powerful. So, we decided to build the whole sandwich for you. It’s
quite a mouthful, but after a few bites of this, peanut butter and jelly
is never going to be the same.
For our example, we decided to implement something we think could have some practical uses. Picture a small company with a sales force of five people who have to share two desks. This is not as cruel as it seems, because these folks spend most of their time on the road, and they are each only in the office for at most one day each week.
Still, when they do get into the office, they’d like the system to know which desks they are sitting at, so that their calls can be directed there. Also, the boss wants to be able to track when they are in the office and control calling privileges from those phones when no one is there.
This need is typically solved by what is
called a hot-desking feature, so we have built one
for you in order to show you the power of func_odbc.
Lets start with the easy stuff, and create
two desktop phones in the sip.conf file:
; sip.conf ; HOT DESK USERS [0000FFFF0001] type=friend host=dynamic secret=my_special_secret context=hotdesk qualify=yes [0000FFFF0002] type=friend host=dynamic secret=my_special_secret context=hotdesk qualify=yes ; END HOT DESK USERS
These two desk phones both enter the
dialplan at the hotdesk context in
extensions.conf. If you want to have these devices actually work, you will
of course need to set the appropriate parameters in the devices
themselves, but we covered all that in Chapter 5, User Device Configuration.
That’s all for sip.conf. We’ve got two slices of bread, which
is hardly a sandwich yet.
Now let’s get the database part of it set up (we are assuming that you have an ODBC database created and working, as outlined in the earlier parts of this chapter). First, connect to the database console.
For PostgreSQL:
$sudo su - postgres$psql -U asterisk -h localhost asteriskPassword:
Then create the table with the following bit of SQL:
CREATE TABLE ast_hotdesk ( id serial NOT NULL, extension int8, first_name text, last_name text, cid_name text, cid_number varchar(10), pin int4, context text, status bool DEFAULT false, "location" text, CONSTRAINT ast_hotdesk_id_pk PRIMARY KEY (id) ) WITHOUT OIDS;
For MySQL:
$mysql -u asterisk -p asteriskEnter password:
Then create the table with the following bit of SQL:
CREATE TABLE ast_hotdesk ( id serial NOT NULL, extension int8, first_name text, last_name text, cid_name text, cid_number varchar(10), pin int4, context text, status bool DEFAULT false, location text, CONSTRAINT ast_hotdesk_id_pk PRIMARY KEY (id) );
The table information is summarized in Table 16.1, “Summary of ast_hotdesk table”.
Table 16.1. Summary of ast_hotdesk table
| Column name | Column type |
|---|---|
id | Serial, auto-incrementing |
extension | Integer |
first_name | Text |
last_name | Text |
cid_name | Text |
cid_number | Varchar 10 |
pin | Integer |
context | Text |
status | Boolean, default false |
location | Text |
After that, populate the database with the following information (some of the values that you see actually will change only after the dialplan work is done, but we include it here by way of example).
At the PostgreSQL console, run the following commands:
asterisk=>INSERT INTO ast_hotdesk ('extension', 'first_name', 'last_name',\ 'cid_name','cid_number', 'pin', 'context', 'location') \ VALUES (1101, 'Leif', 'Madsen', 'Leif Madsen', '4165551101', '555',\ 'longdistance','0000FFFF0001');
At the MySQL console, run the following commands:
mysql>INSERT INTO ast_hotdesk (extension, first_name, last_name, cid_name, cid_number, pin, context, location) VALUES (1101, 'Leif', 'Madsen', 'Leif Madsen', '4165551101', '555', 'longdistance', '0000FFFF0001');
Repeat
these commands, changing the VALUES as
needed, for all entries you wish to have in the database. You can view the
data in the ast_hotdesk table by
running a simple SELECT statement from
the database console:
mysql>SELECT * FROM ast_hotdesk;
which would give you something like the following output:
| id | extension | first_name | last_name | cid_name | cid_number
|----+-----------+------------+----------------+--------------------+--------------
| 1 | 1101 | "Leif" | "Madsen" | "Leif Madsen" | "4165551101"
| 2 | 1102 | "Jim" | "Van Meggelen" | "Jim Van Meggelen" | "4165551102"
| 3 | 1103 | "Russell" | "Bryant" | "Russell Bryant" | "4165551103"
| 4 | 1104 | "Mark" | "Spencer" | "Mark Spencer" | "4165551104"
| 5 | 1105 | "Kevin" | "Fleming" | "Kevin Fleming" | "4165551105"
| pin | context | status | location |$
+-------+-----------------+---------+----------------+
| "555" | "longdistance" | "TRUE" | "0000FFFF0001" |
| "556" | "longdistance" | "FALSE" | "" |
| "557" | "local" | "FALSE" | "" |
| "558" | "international" | "FALSE" | "" |
| "559" | "local" | "FALSE" | "" |We’ve got the condiments now, so let’s get to our dialplan. This is where the magic is going to happen.
Somewhere in extensions.conf we are going to have to create
the hotdesk context. To start, let’s
define a pattern-match extension that will allow the users to log
in:
; extensions.conf
; Hot-Desking Feature
[hotdesk]
; Hot Desk Login
exten => _#110[1-5],1,NoOp()
same => n,Set(E=${EXTEN:1}) ; strip off the leading hash (#) symbol
same => n,Verbose(1,Hot Desk Extension ${E} is changing status)
same => n,Verbose(1,Checking current status of extension ${E})
same => n,Set(${E}_STATUS=${HOTDESK_INFO(status,${E})})
same => n,Set(${E}_PIN=${HOTDESK_INFO(pin,${E})})We’re not done writing this extension yet, but let’s pause for a moment and see where we’re at so far.
When a sales agent sits down at a desk, he
logs in by dialing hash (#) plus his own extension number. In this case we
have allowed the 1101 through 1105 extensions to log in with our pattern
match of _#110[1-5]. You could just as
easily make this less restrictive by using _#11XX (allowing 1100 through 1199). This
extension uses func_odbc to perform a
lookup with the HOTDESK_INFO() dialplan
function. This custom function (which we will define in the func_odbc.conf file) performs an SQL statement
and returns whatever is retrieved from the database.
We would define the new function HOTDESK_INFO() in func_odbc.conf like so:
[INFO]
prefix=HOTDESK
dsn=asterisk
readsql=SELECT ${ARG1} FROM ast_hotdesk WHERE extension = '${ARG2}'That’s a lot of stuff in just a few lines. Let’s quickly cover them before we move on.
First of all, the prefix is optional. If you don’t configure the
prefix, then Asterisk adds “ODBC” to the name of the function (in this case,
INFO), which means this function would
become ODBC_INFO(). This is not very
descriptive of what the function is doing, so it can be helpful to assign
a prefix that helps to relate your ODBC functions to the tasks they are
performing. We chose HOTDESK, which
means that this custom function will be named HOTDESK_INFO().
The dsn
attribute tells Asterisk which connection to use from res_odbc.conf. Since several database
connections could be configured in res_odbc.conf, we specify which one to use
here. In Figure 16.1, “Relationships between func_odbc.conf, res_odbc.conf,
/etc/odbc.ini (unixODBC), and the database connection”, we show the relationship
between the various file configurations and how they reference down the
chain to connect to the database.
Tip
The func_odbc.conf.sample file in the Asterisk source contains additional
information about how to handle multiple databases and control the
reading and writing of information to different DSN connections.
Specifically, the readhandle,
writehandle, readsql, and writesql arguments will provide you with
great flexibility for database integration and control.
Finally, we define our SQL statement with the readsql attribute. Dialplan functions have two
different formats that they can be called with: one for retrieving
information, and one for setting information. The readsql attribute is used when we call the
HOTDESK_INFO() function with the
retrieve format (we could execute a separate SQL statement with the
writesql attribute; we’ll discuss the
format for that attribute a little bit later in this chapter).
Reading values from this function would take this format in the dialplan:
exten => s,n,Set(RETURNED_VALUE=${HOTDESK_INFO(status,1101)})This
would return the value located in the database within the status column where the extension column equals 1101. The status and 1101 we pass to the HOTDESK_INFO() function are then placed
into the SQL statement we assigned to the readsql attribute, available as ${ARG1} and ${ARG2}, respectively. If we had passed a third
option, this would have been available as ${ARG3}.
Figure 16.1. Relationships between func_odbc.conf, res_odbc.conf, /etc/odbc.ini (unixODBC), and the database connection
After the SQL statement is executed, the
value returned (if any) is assigned to the RETURNED_VALUE channel variable.
So, in the first two lines of the following
block of code, we are passing the value status and the value contained in the
${E} variable (e.g., 1101) to the HOTDESK_INFO() function. The two values are then
replaced in the SQL statement with ${ARG1} and ${ARG2}, respectively, and the SQL statement is
executed. Finally the value returned is assigned to the ${E}_STATUS channel variable.
OK, let’s finish writing the pattern-match extension now:
same => n,Set(${E}_STATUS=${HOTDESK_INFO(status,${E})})
same => n,Set(${E}_PIN=${HOTDESK_INFO(pin,${E})})
same => n,GotoIf($[${ODBCROWS} < 0]?invalid_user,1)
; check if ${E}_STATUS is NULL
same => n,GotoIf($[${${E}_STATUS} = 1]?logout,1:login,1)After
assigning the value of the status
column to the ${E}_STATUS variable (if
the user dials extension 1101, the
variable name will be 1101_STATUS), we
check if we’ve received a value back from the database (error checking)
using the ${ODBCROWS} channel
variable.
The last row in the block checks the status
of the phone and, if the agent is currently logged in, logs him off. If
the agent is not already logged in, it will go to extension login, priority 1 within the same context.
Tip
Remember that in a traditional phone system all extensions must be numbers, but in Asterisk, extensions can have names as well. A possible advantage of using an extension that’s not a number is that it will be much harder for a user to dial it from her phone and, thus, more secure. We’re going to use several named extensions in this example. If you want to be absolutely sure that a malicious user cannot access those named extensions, simply use the trick that the AEL loader uses: start with a priority other than 1. You can access the first line of the extension by assigning it a priority label and referencing it via the extension name/priority label combination.
The login
extension runs some initial checks to verify the pin code entered by the
agent. We allow him three tries to enter the correct pin, and if all tries
are invalid we send the call to the login_fail extension (which we will be writing
later):
exten => login,1,NoOp() ; set initial counter values
same => n,Set(PIN_TRIES=1) ; pin tries counter
same => n,Set(MAX_PIN_TRIES=3) ; set max number of login attempts
same => n,Playback(silence/1) ; play back some silence so first prompt is
; not cut off
same => n(get_pin),NoOp()
same => n,Set(PIN_TRIES=$[${PIN_TRIES} + 1]) ; increase pin try counter
same => n,Read(PIN_ENTERED,enter-password,${LEN(${${E}_PIN})})
same => n,GotoIf($["${PIN_ENTERED}" = "${${E}_PIN}"]?valid_login,1)
same => n,Playback(pin-invalid)
same => n,GotoIf($[${PIN_TRIES} <= ${MAX_PIN_TRIES}]?get_pin:login_fail,1)
If the pin entered matches, we validate the
login with the valid_login extension.
First we utilize the CHANNEL variable
to figure out which phone device the agent is calling from. The CHANNEL variable is usually populated with
something like SIP/0000FFFF0001-ab4034c, so we make use of the
CUT() function to first pull off the
SIP/ portion of the string and assign
that to LOCATION. We then strip off the
-ab4034c part of the string, discard
it, and assign the remainder (0000FFFF0001) to the LOCATION variable:
exten => valid_login,1,NoOp()
; CUT off the channel technology and assign it to the LOCATION variable
same => n,Set(LOCATION=${CUT(CHANNEL,/,2)})
; CUT off the unique identifier and save the remainder to the LOCATION variable
same => n,Set(LOCATION=${CUT(LOCATION,-,1)})We utilize yet another custom function
created in the func_odbc.conf file,
HOTDESK_CHECK_PHONE_LOGINS(), to check
if any other users were previously logged into this phone and forgot to
log out. If the number of logged-in users is greater than 0 (it should never be more than 1, but we check for higher values anyway and
reset those, too), it runs the logic in the logout_login extension:
; func_odbc.conf
[CHECK_PHONE_LOGINS]
prefix=HOTDESK
dsn=asterisk
; *** This line should have no line breaks
readsql=SELECT COUNT(status) FROM ast_hotdesk WHERE status = '1' AND
location = '${ARG1}'If there are no other agents logged into
the device, we update the login status for this user with the HOTDESK_STATUS() function:
; Continuation of the valid_login extension below
same => n,Set(USERS_LOGGED_IN=${HOTDESK_CHECK_PHONE_
LOGINS(${LOCATION})})
same => n,GotoIf($[${USERS_LOGGED_IN} > 0]?logout_login,1)
same => n(set_login_status),NoOp()
; Set the status for the phone to '1' and where the agent is logged into
same => n,Set(HOTDESK_STATUS(${E})=1,${LOCATION})
same => n,GotoIf($[${ODBCROWS} < 1]?error,1)
same => n,Playback(agent-loginok)
same => n,Hangup()We create a write function in func_odbc.conf like so:
[STATUS]
prefix=HOTDESK
dsn=asterisk
; *** This line should have no line breaks
writesql=UPDATE ast_hotdesk SET status = '${VAL1}',
location = '${VAL2}' WHERE extension = '${ARG1}'The syntax is very similar to the readsql syntax discussed earlier in the chapter,
but there are a few new things here, so let’s discuss them before moving
on.
The first thing you may have noticed is that
we now have both ${VALx} and ${ARGx} variables in our SQL statement. These contain
the values we pass to the function from the dialplan. In this case, we
have two VAL variables and a single
ARG variable that were set from the
dialplan via this statement:
Set(HOTDESK_STATUS(${E})=1,${LOCATION})Notice the syntax is slightly different from that of the read-style function. This signals to Asterisk that you want to perform a write (this is the same syntax as that used for other dialplan functions).
We are passing the value of the ${E} variable to the HOTDESK_STATUS() function, whose value is then
accessible in the SQL statement within func_odbc.conf with the ${ARG1} variable. We then pass two values:
1 and ${LOCATION}. These are available to the SQL
statement in the ${VAL1} and ${VAL2} variables, respectively.
As mentioned previously, if we had to log out
one or more agents before logging this one in, we would check this with
the logout_login extension. This
dialplan logic will utilize the ODBC_FETCH() function to pop information off the
information stack returned by the HOTDESK_LOGGED_IN_USER() function. More than
likely this will execute only one loop, but it’s a good example of how you
might update or parse multiple rows in the database.[148]
The first part of our dialplan returns an ID
number that we can use with the ODBC_FETCH() function to iterate through the
values returned. We’re going to assign this ID to the LOGGED_IN_ID channel variable:
same => n,Set(LOGGED_IN_ID=${HOTDESK_LOGGED_IN_USER(${LOCATION})})Here
is the logout_login extension, which
could potentially loop through multiple rows:
exten => logout_login,1,NoOp()
; set all logged-in users on this device to logged-out status
same => n,Set(LOGGED_IN_ID=${HOTDESK_LOGGED_IN_USER(${LOCATION})})
same => n(start_loop),NoOp()
same => n,Set(WHO=${ODBC_FETCH(${LOGGED_IN_ID})})
same => n,GotoIf($["${ODBC_FETCH_STATUS}" = "FAILURE"]?cleanup)
same => n,Set(HOTDESK_STATUS(${WHO})=0) ; log out phone
same => n,Goto(start_loop)
same => n(cleanup),ODBCFinish(${LOGGED_IN_ID})
same => n,Goto(valid_login,set_login_status) ; return to logging in
We assign the first value returned from the database (e.g.,
the extension 1101) to the WHO channel. Before doing anything, though, we
check to see if the ODBC_FETCH()
function was successful in returning data. If the ODBC_FETCH_STATUS channel variable contains
FAILURE, we have no data to work with,
so we move to the cleanup priority
label.
If we have data, we then pass the value of
${WHO} as an argument to the HOTDESK_STATUS() function, which contains a value
of 0. This is the first value passed to
HOTDESK_STATUS() and is shown as ${VAL1} in func_odbc.conf, where the function is
declared.
If you look at the HOTDESK_STATUS() function in func_odbc.conf you will see we could also pass
a second value, but we’re not doing that here since we want to remove any
values from that column in order to log out the user, which setting no
value does effectively.
After using HOTDESK_STATUS() to log out the user, we return
to the start_loop priority label to
loop through all values, which simply executes a NoOp(). After attempting to retrieve a value, we
again check ODBC_FETCH_STATUS for
FAILURE. If that value is found, we
move to the cleanup priority label,
where we execute the ODBCFinish()
dialplan application to perform cleanup. We then return to the valid_login extension at the set_login_status priority label.
The rest of the context should be fairly
straightforward (if some of this doesn’t make sense, we suggest you go
back and refresh your memory with Chapters 6 and 10). The one
trick you may be unfamiliar with could be the usage of the ${ODBCROWS} channel variable, which is set by
the HOTDESK_STATUS() function. This
tells us how many rows were affected in the SQL UPDATE, which we assume to be 1. If the value of ${ODBCROWS} is less than 1, we assume an error and handle it
appropriately:
exten => logout,1,NoOp()
same => n,Set(HOTDESK_STATUS(${E})=0)
same => n,GotoIf($[${ODBCROWS} < 1]?error,1)
same => n,Playback(silence/1&agent-loggedoff)
same => n,Hangup()
exten => login_fail,1,NoOp()
same => n,Playback(silence/1&login-fail)
same => n,Hangup()
exten => error,1,NoOp()
same => n,Playback(silence/1&connection-failed)
same => n,Hangup()
exten => invalid_user,1,NoOp()
same => n,Verbose(1,Hot Desk extension ${E} does not exist)
same => n,Playback(silence/2&invalid)
same => n,Hangup()We also include the hotdesk_outbound context, which will handle our
outgoing calls after we have logged the agent into the system:
include => hotdesk_outbound
The
hotdesk_outbound context utilizes many
of the same principles discussed previously, so we won’t approach it quite
so thoroughly; essentially, this context will catch all numbers dialed
from the desk phones. We first set our LOCATION variable using the CHANNEL variable, then determine which extension
(agent) is logged into the system and assign that value to the WHO variable. If this variable is NULL, we reject the outgoing call. If it is not
NULL, then we get the agent information
using the HOTDESK_INFO() function and
assign it to several CHANNEL variables,
including the context to handle the call with, where we perform a Goto() to the context we have been assigned
(which controls our outbound access).
We will make use of the HOTDESK_PHONE_STATUS() dialplan function, which
you can define in func_odbc.conf like
so:
[PHONE_STATUS]
prefix=HOTDESK
dsn=asterisk
readsql=SELECT extension FROM ast_hotdesk WHERE status = '1'
readsql+= AND location = '${ARG1}'If we try to dial a number
that is not handled by our context (or one of the transitive
contexts—i.e., international contains long distance, which also contains
local), the built-in extension i is
executed, which plays back a message stating that the action cannot be
performed and hangs up the call:
[hotdesk_outbound]
exten => _X.,1,NoOp()
same => n,Set(LOCATION=${CUT(CHANNEL,/,2)})
same => n,Set(LOCATION=${CUT(LOCATION,-,1)})
same => n,Set(WHO=${HOTDESK_PHONE_STATUS(${LOCATION})})
same => n,GotoIf($[${ISNULL(${WHO})}]?no_outgoing,1)
same => n,Set(${WHO}_CID_NAME=${HOTDESK_INFO(cid_name,${WHO})})
same => n,Set(${WHO}_CID_NUMBER=${HOTDESK_INFO(cid_number,${WHO})})
same => n,Set(${WHO}_CONTEXT=${HOTDESK_INFO(context,${WHO})})
same => n,Goto(${${WHO}_CONTEXT},${EXTEN},1)
[international]
exten => _011.,1,NoOp()
same => n,Set(E=${EXTEN})
same => n,Goto(outgoing,call,1)
exten => i,1,NoOp()
same => n,Playback(silence/2&sorry-cant-let-you-do-that2)
same => n,Hangup()
include => longdistance
[longdistance]
exten => _1NXXNXXXXXX,1,NoOp()
same => n,Set(E=${EXTEN})
same => n,Goto(outgoing,call,1)
exten => _NXXNXXXXXX,1,Goto(1${EXTEN},1)
exten => i,1,NoOp()
same => n,Playback(silence/2&sorry-cant-let-you-do-that2)
same => n,Hangup()
include => local
[local]
exten => _416NXXXXXX,1,NoOp()
same => n,Set(E=${EXTEN})
same => n,Goto(outgoing,call,1)
exten => i,1,NoOp()
same => n,Playback(silence/2&sorry-cant-let-you-do-that2)
same => n,Hangup()If the call is allowed to be
executed, it is sent to the [outgoing]
context for processing and the caller ID name and number are set with the
CALLERID() function. The call is then placed via the
SIP channel using the service_provider
we created in the sip.conf
file:
[outgoing]
exten => call,1,NoOp()
same => n,Set(CALLERID(name)=${${WHO}_CID_NAME})
same => n,Set(CALLERID(number)=${${WHO}_CID_NUMBER})
same => n,Dial(SIP/service_provider/${E})
same => n,Playback(silence/2&pls-try-call-later)
same => n,Hangup()Our service_provider might look something like this
in sip.conf:
[service_provider] type=friend host=switch1.service_provider.net defaultuser=my_username fromuser=my_username secret=welcome context=incoming canreinvite=no disallow=all allow=ulaw
Now that we’ve implemented a fairly complex
feature in the dialplan with the help of func_odbc to retrieve and store data in a remote
relational database, hopefully you’re starting to get why we think this is
so cool. With a handful of self-defined dialplan functions in the
func_odbc.conf file and a couple of
tables in a database, we can create some fairly rich applications!
How many things have you just thought of that
you could apply func_odbc to?
Using Realtime
The Asterisk Realtime Architecture (ARA) enables you to
store the configuration files (that would normally be found in /etc/asterisk) and their configuration options
in a database table. There are two types of realtime:
static and dynamic.
The static version is similar to the traditional method of reading a configuration file, except that the data is read from the database instead.
The dynamic realtime method, which loads and updates the information as it is required, is used for things such as SIP/IAX2 user and peer objects and voicemail.
Making changes to static information requires
a reload, just as if you had changed a text file on the system, but
dynamic information is polled by Asterisk as needed, so no reload is
required when changes are made to this data. Realtime is configured in the
extconfig.conf file located in the
/etc/asterisk directory. This file
tells Asterisk what to load from the database and where to load it from,
allowing certain files to be loaded from the database and other files to
be loaded from the standard configuration files.
Static Realtime
Static realtime is useful when you want to load from a
database the configuration that you would normally place in the
configuration files in /etc/asterisk. The same rules that apply to
flat files on your system still apply when using static realtime. For
example, after making changes to the configuration you must either run
the reload command from the Asterisk CLI, or reload the module associated
with the configuration file (e.g., using module reload chan_sip.so).
When using static realtime, we tell
Asterisk which files we want to load from the database using the
following syntax in the extconfig.conf file:
; /etc/asterisk/extconfig.conf filename.conf => driver,database[,table]
Note
If the table name is not specified, Asterisk will use the name of the file as the table name instead.
The static realtime module uses a specifically formatted table to read the configuration of static files in from the database. Table 16.2, “Table layout and description of ast_config” illustrates the columns as they should be defined in your database:
Table 16.2. Table layout and description of ast_config
A simple file we can load from static
realtime is the musiconhold.conf[149] file. Let’s start by moving this file to a temporary
location:
$cd /etc/asterisk$mv musiconhold.conf musiconhold.conf.old
In order for the classes to be removed from memory, we need to restart Asterisk. Then we can verify that our classes are blank by running moh show classes:
*CLI>core restart now*CLI>moh show classes*CLI>
Let’s put the [default] class back into Asterisk, but now
we’ll load it from the database. Connect to your database and execute
the following INSERT
statements:
>INSERT INTO ast_config (filename,category,var_name,var_val) VALUES ('musiconhold.conf','default','mode','files');>INSERT INTO ast_config (filename,category,var_name,var_val) VALUES ('musiconhold.conf','default','directory','/var/lib/asterisk/moh');
You
can verify that your values have made it into the database by running a
SELECT statement:
asterisk=#SELECT filename,category,var_name,var_val FROM ast_config;filename | category | var_name | var_val ------------------+----------------+--------------+------------------------ musiconhold.conf | default | mode | files musiconhold.conf | default | directory | /var/lib/asterisk/moh (2 rows)
There’s one last thing to modify in the
extconfig.conf file in the
/etc/asterisk directory to tell
Asterisk to get the data for musiconhold.conf from the database using the
ODBC connection. The first column states that we’re using the ODBC
drivers to connect (res_odbc.conf)
and that the connection name is asterisk (as defined with [asterisk] in res_odbc.conf). Add the following line to the
end of the extconfig.conf file, and
then save it:
musiconhold.conf => odbc,asterisk,ast_config
Then connect to the Asterisk console and perform a reload:
*CLI>module reload res_musiconhold.so
You can now verify that your music on hold classes are loading from the database by running moh show classes:
*CLI>moh show classesClass: general Mode: files Directory: /var/lib/asterisk/moh
And there
you go: musiconhold.conf loaded
from the database. If you have issues with the reload of the module
loading the data into memory, try restarting Asterisk. You can perform
the same steps in order to load other flat files from the database, as
needed.
Dynamic Realtime
The dynamic realtime system is used to load objects that may change often, such as SIP/IAX2 users and peers, queues and their members, and voicemail messages. Likewise, when new records are likely to be added on a regular basis, we can utilize the power of the database to let us load this information on an as-needed basis.
All of realtime is configured in the
/etc/asterisk/extconfig.conf file, but dynamic realtime has well-defined configuration
names. Defining something like SIP peers is done with the following
format:
; extconfig.conf sippeers =>driver,database[,table]
The
table name is optional. If it is omitted, Asterisk will use the
predefined name (i.e., sippeers) to
identify the table in which to look up the data.
Tip
Remember that we have both SIP peers and SIP users: peers are end-points we send calls to, and a user is something we receive calls from. A friend is shorthand that defines both.
In our example, we’ll be using the ast_sippeers table to store our SIP peer
information. So, to configure Asterisk to load all SIP peers from our
database using realtime, we would define something like this:
; extconfig.conf sippeers => odbc,asterisk,ast_sipfriends
To also load our
SIP users from the database, we would define the sipusers object like so:
sipusers => odbc,asterisk,ast_sipfriends
You
may have noticed we used the same table for both the sippeers and sipusers. This is because there will be a
type field (just as if we were
defining the type in the sip.conf file) that will
let us define a type of user,
peer, or friend. If you unload chan_sip.so and then load it back into memory
(i.e., using module unload
chan_sip.so followed by module load
chan_sip.so) after configuring extconfig.conf, you will be greeted with some
warnings telling you which columns you’re missing for the realtime
table. Since we’ve enabled sippeers
and sipusers in extconfig.conf, we will get the following on
the console (which has been trimmed due to space requirements):
WARNING: Realtime table ast_sipfriends@asterisk requires column 'name', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'ipaddr', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'port', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'regseconds', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'defaultuser', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'fullcontact', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'regserver', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'useragent', but that column does not exist! WARNING: Realtime table ast_sipfriends@asterisk requires column 'lastms', but that column does not exist!
As you can see, we are missing several columns from the table
ast_sipfriends, which we’ve defined
as connecting to the asterisk object
as defined in res_odbc.conf. The
next step is to create our ast_sipfriends table with all the columns
listed by the warning messages, in addition to the following: the
type column, which is required to
define users, peers, and friends; the secret column, which is used for setting a
password; and the host column, which
allows us to define whether the peer is dynamically registering to us or
has a static IP address. Table 16.3, “Minimal sippeers/sipusers realtime table” lists all
of the columns that should appear in our table, and their types.
Table 16.3. Minimal sippeers/sipusers realtime table
| Column name | Column type |
|---|---|
type | Varchar 6 |
name | Varchar 128 |
secret | Varchar 128 |
context | Varchar 128 |
host | Varchar 128 |
ipaddr | Varchar 128 |
port | Varchar 5 |
regseconds | Bigint |
defaultuser | Varchar 128 |
fullcontact | Varchar 128 |
regserver | Varchar 128 |
useragent | Varchar 128 |
lastms | Integer |
For each peer you want to register, you
need to insert data in the columns type, name,
secret, context, host, and defaultuser. The rest of the columns will be
populated automatically when the peer registers.
The port, regseconds, and ipaddr fields are required to let Asterisk
store the registration information for the peer so it can determine
where to send the calls. (Note that if the peer is static, you will have to populate the ipaddr field yourself.) The port field is optional and defaults to the
standard port defined in the [general] section, and the regseconds will remain blank. Table 16.4, “Example information used to populate the ast_sipfriends
table” lists some sample values that we’ll use
to populate our ast_sipfriends
table.
Table 16.4. Example information used to populate the ast_sipfriends table
| Column name | Value |
|---|---|
type | friend |
name | 0000FFFF0008 |
defaultuser | 0000FFFF0008 |
host | dynamic |
secret | welcome |
context | LocalSets |
Prior to registering your peer, though, you
need to enable realtime caching in sip.conf. Otherwise, the peer will not be
loaded into memory, and the registration will not be remembered. If your
peers only place calls and don’t need to register to your system, you
don’t need to enable realtime caching because the peers will be checked
against the database each time they place a call. However, if you load
your peers into memory, the database will only need to be contacted on
initial registration, and after the registration expires.
Additional options in sip.conf exist for realtime peers. These are defined in the
[general]
section and described in Table 16.5, “Realtime options in sip.conf”.
Table 16.5. Realtime options in sip.conf
| Configuration option | Description |
|---|---|
rtcachefriends | Caches peers in memory on an as-needed basis after
they have contacted the server. That is, on Asterisk start, the
peers are not loaded into memory automatically; only after a
peer has contacted the server (e.g., via a registration or phone
call) is it loaded in memory. Values are yes or no. |
rtsavesysname | When a peer registers to the system, saves the
systemname (as defined in
asterisk.conf) into the
regserver field within the
database. (See Setting the systemname for Globally Unique IDs for more
information.) Using regserver
is useful when you have multiple servers registering peers to
the same table. Values are yes or no. |
rtupdate | Sends registration information such as the IP
address, the origination port, the registration period, and the
username of the user-agent to
the database when a peer registers to Asterisk. Values are
yes or no, and the default is yes. |
rtautoclear | Automatically expires friends on the same schedule
as if they had just registered. This causes a peer to be removed
from memory when the registration period has expired, until that
peer is requested again (e.g., via registration or placing a
call). Values are yes,
no, or an integer value that
causes the peers to be removed from memory after that number of
seconds instead of the registration interval. |
ignoreregexpire | When enabled, peers are not removed from memory when the registration period expires. Instead, the information is left in memory so that if a call is requested to an endpoint that has an expired registration, the last known information (IP address, port, etc.) will be tried. |
After enabling rtcachefriends=yes in sip.conf and reloading chan_sip.so (using module reload chan_sip.so), you can register
your peer to Asterisk using realtime, and the peer should then be
populated into memory. You will be able to verify this by executing the
sip show peers command on the
Asterisk console:
Name/username Host Dyn Port Status Realtime 0000FFFF0008/0000FFFF0008 172.16.0.160 D 5060 Unmonitored Cached RT
If you were to look at the table in the database directly, you would see something like this:
+--------+--------------+---------+-----------+---------+--------------+ | type | name | secret | context | host | ipaddr | +--------+--------------+---------+-----------+---------+--------------+ | friend | 0000FFFF0008 | welcome | LocalSets | dynamic | 172.16.0.160 | +--------+--------------+---------+-----------+---------+--------------+ +------+------------+--------------+-------------------------------------+ | port | regseconds | defaultuser | fullcontact | +------+------------+--------------+-------------------------------------+ | 5060 | 1283928895 | 0000FFFF0008 | sip:0000FFFF0008@172.16.0.160:52722 | +------+------------+--------------+-------------------------------------+ +-----------+-----------------+--------+ | regserver | useragent | lastms | +-----------+-----------------+--------+ | NULL | Zoiper rev.6739 | 0 | +-----------+-----------------+--------+
There are many more options
for that we can define for SIP friends, such as the caller ID; adding
that information is as simple as adding a callerid column to the table. See the
sip.conf.sample file for more options
that can be defined for SIP friends.
Storing Call Detail Records (CDRs)
Call detail records (CDRs) contain information about calls that have passed through your Asterisk system. They are discussed further in Chapter 24, System Monitoring and Logging. Storing CDRs is a popular use of databases in Asterisk, because it makes them easier to manage (for example, you can keep track of many Asterisk systems in a single table). Also, by placing records into a database you open up many possibilities, including building your own web interface for tracking statistics such as call usage and most-called locations, billing, or phone company invoice verification.
The best way to store your call detail
records is via the cdr_adaptive_odbc
module. This module allows you to choose which columns of data built into
Asterisk are stored in your table, and permits you to add additional
columns that you can populate with the CDR() dialplan function. You can even store different parts of CDR data to different
tables and databases, if that is required.
More information about the standard CDR
columns in Asterisk is available in Table 24.2, “Default CDR fields”.
You can define all or any subset of these records in the database, and
Asterisk will work around what is available. You can also add additional
columns to store other data relevant to the calls. For example, if you
wanted to implement least cost routing (LCR), you could add columns for
route, per-minute cost, and per-minute rate. Once you’ve added those
columns, they can be populated via the dialplan by using the CDR() function (e.g., Set(CDR(per_minute_rate)=0.01)).
After creating your table in the database
(which we’ll assume you’ve called cdr),
you need to configure the cdr_adaptive_odbc.conf file in the /etc/asterisk/ folder. The following example
will utilize the asterisk connection
we’ve defined in res_odbc.conf and
store the data in the cdr table:
; cdr_adaptive_odbc.conf [adaptive_connection] connection=asterisk table=cdr
Yes, really, that’s all you need. After configuring
cdr_adaptive_odbc.conf, just reload
the cdr_adaptive_odbc.so module from
the Asterisk console by running module reload
cdr_adaptive_odbc.so. You can verify that the Adaptive ODBC backend has been
loaded by running cdr show
status:
*CLI>cdr show statusCall Detail Record (CDR) settings ---------------------------------- Logging: Enabled Mode: Simple Log unanswered calls: No * Registered Backends ------------------- cdr-syslogAdaptive ODBCcdr-custom csv cdr_manager
Now place a call that gets answered (e.g., using
Playback(), or Dial()ing another channel and answering it). You
should get some CDRs stored into your database. You can check by running
SELECT * FROM CDR; from your database
console.
With the basic CDR information stored into
the database, you might want to add some additional information to the
cdr table, such as the route rate. You
can use the ALTER TABLE directive to
add a column called route_rate to the
table:
sql>ALTER TABLE cdr ADD COLUMN route_rate varchar(10);
Now
reload the cdr_adaptive_odbc.so
module from the Asterisk console:
*CLI>module reload cdr_adaptive_odbc.so
and
populate the new column from the Asterisk dialplan using the CDR() function, like so:
exten => _NXXNXXXXXX,1,Verbose(1,Example of adaptive ODBC usage)
same => n,Set(CDR(route_rate)=0.01)
same => n,Dial(SIP/my_itsp/${EXTEN})
same => n,Hangup()After the alteration to your database and dialplan, you can place a call and then look at your CDRs. You should see something like the following:
+--------------+----------+---------+------------+ | src | duration | billsec | route_rate | +--------------+----------+---------+------------+ | 0000FFFF0008 | 37 | 30 | 0.01 | +--------------+----------+---------+------------+
You now have enough information to calculate how much the call should have cost you, which enables you to either bill customers or check your records against what the phone company is sending you, so you can do monthly auditing of your phone bills.
ODBC Voicemail
Asterisk enables you to store voicemail inside the database using the ODBC connector. This is useful in a clustered environment where you want to abstract the voicemail data from the local system so that multiple Asterisk boxes have access to the same data. Of course, you have to take into consideration that you are centralizing a part of Asterisk, and you need to take actions to protect that data, such as making regular backups and possibly clustering the database backend using replication.
Asterisk stores each voicemail message inside a Binary Large Object (BLOB). When retrieving the data, it pulls the information out of the BLOB and temporarily stores it on the hard drive while it is being played back to the user. Asterisk then removes the BLOB and the record from the database when the user deletes the voicemail. Many databases, such as MySQL, contain native support for BLOBs, but as you’ll see, with PostgreSQL a couple of extra steps are required to utilize this functionality that we’ll explore in this section. When you’re done, you’ll be able to record, play back, and delete voicemail data from the database just as if it were stored on the local hard drive.
Note
This section builds upon previous
configuration sections in this chapter. If you have not already done
so, be sure to follow the steps in the sections the section called “Installing PostgreSQL for CentOS” and the section called “Installing and Configuring ODBC” before continuing. In the
latter section, be sure you have enabled ODBC_STORAGE in the menuselect system under
Voicemail Options.
Creating the Large Object Type for PostgreSQL
While MySQL has a BLOB (Binary Large OBject) type, we have to tell PostgreSQL how to handle large objects. This includes creating a trigger to clean up the data when we delete from the database a record that references a large object.
Connect to the database as the asterisk user from the console:
$psql -h localhost -U asterisk asteriskPassword:
Note
You must be a
superuser to execute the following code. Also,
if you use the postgres user to
create the table, you will need to use the ALTER TABLE SQL directive to change the
owner to the asterisk user.
At the PostgreSQL console, run the following script to create the large object type:
CREATE FUNCTION loin (cstring) RETURNS lo AS 'oidin' LANGUAGE internal IMMUTABLE STRICT; CREATE FUNCTION loout (lo) RETURNS cstring AS 'oidout' LANGUAGE internal IMMUTABLE STRICT; CREATE FUNCTION lorecv (internal) RETURNS lo AS 'oidrecv' LANGUAGE internal IMMUTABLE STRICT; CREATE FUNCTION losend (lo) RETURNS bytea AS 'oidrecv' LANGUAGE internal IMMUTABLE STRICT; CREATE TYPE lo ( INPUT = loin, OUTPUT = loout, RECEIVE = lorecv, SEND = losend, INTERNALLENGTH = 4, PASSEDBYVALUE ); CREATE CAST (lo AS oid) WITHOUT FUNCTION AS IMPLICIT; CREATE CAST (oid AS lo) WITHOUT FUNCTION AS IMPLICIT;
We’ll be making use of the PostgreSQL procedural language called pgSQL/PL to create a function. This function will be called from a trigger that gets executed whenever we modify or delete a record in the table used to store voicemail messages. This is so the data is cleaned up and not left as an orphan in the database:
CREATE FUNCTION vm_lo_cleanup() RETURNS "trigger"
AS $$
declare
msgcount INTEGER;
begin
-- raise notice 'Starting lo_cleanup function for large object with oid
%',old.recording;
-- If it is an update action but the BLOB (lo) field was not changed,
don't do anything
if (TG_OP = 'UPDATE') then
if ((old.recording = new.recording) or (old.recording is NULL)) then
raise notice 'Not cleaning up the large object table,
as recording has not changed';
return new;
end if;
end if;
if (old.recording IS NOT NULL) then
SELECT INTO msgcount COUNT(*) AS COUNT FROM voicemessages WHERE recording
= old.recording;
if (msgcount > 0) then
raise notice 'Not deleting record from the large object table, as object
is still referenced';
return new;
else
perform lo_unlink(old.recording);
if found then
raise notice 'Cleaning up the large object table';
return new;
else
raise exception 'Failed to clean up the large object table';
return old;
end if;
end if;
else
raise notice 'No need to clean up the large object table,
no recording on old row';
return new;
end if;
end$$
LANGUAGE plpgsql;We’re going to create a table called
voicemessages where the voicemail
information will be stored:
CREATE TABLE voicemessages ( uniqueid serial PRIMARY KEY, msgnum int4, dir varchar(80), context varchar(80), macrocontext varchar(80), callerid varchar(40), origtime varchar(40), duration varchar(20), mailboxuser varchar(80), mailboxcontext varchar(80), recording lo, label varchar(30), "read" bool DEFAULT false, flag varchar(10) );
And now we need to associate a trigger with our newly
created table in order to perform cleanup whenever we change or delete a
record in the voicemessages
table:
CREATE TRIGGER vm_cleanup AFTER DELETE OR UPDATE ON voicemessages FOR EACH ROW EXECUTE PROCEDURE vm_lo_cleanup();
ODBC Voicemail Storage Table Layout
We’ll be utilizing the voicemessages table for storing our voicemail
information in an ODBC-connected database. Table 16.6, “ODBC voicemail storage table layout” describes the table configuration for
ODBC voicemail storage. If you’re using a PostgreSQL database, the table
definition and large object support were configured in the preceding
section.
Table 16.6. ODBC voicemail storage table layout
| Column name | Column type |
|---|---|
uniqueid | Serial, primary key |
dir | Varchar 80 |
msgnum | Integer |
recording | BLOB (Binary Large OBject) |
context | Varchar 80 |
macrocontext | Varchar 80 |
callerid | Varchar 40 |
origtime | Varchar 40 |
duration | Varchar 20 |
mailboxuser | Varchar 80 |
mailboxcontext | Varchar 80 |
label | Varchar 30 |
read | Boolean, default false[a] |
flag | Varchar 10 |
[a] | |
Configuring voicemail.conf for ODBC Storage
There isn’t much to add to the voicemail.conf file to enable the ODBC
voicemail storage. In fact, it’s only three lines! Normally, you
probably have multiple format types defined in the [general] section of voicemail.conf, but we need to set this to a
single format because we can only save one file (format) to the
database. The wav49 format is a
compressed WAV file format that should be playable on both Linux and
Microsoft Windows desktops.
The odbcstorage option points at the name you defined in the res_odbc.conf file (if you’ve been following along in this chapter,
then we called it asterisk). The odbctable option refers to the table where
voicemail information should be stored. In the examples in this chapter
we use the table named voicemessages:
[general] format=wav49 odbcstorage=asterisk odbctable=voicemessages
You may want to create a separate
voicemail context, or you can utilize the default voicemail context. Alternatively, you
can skip creating a new user and use an existing user, such as 0000FFFF0001. We’ll define the mailbox in the
default voicemail context like so:
[default] 1000 => 1000,J.P. Wiser
Tip
You can also use the voicemail definition in extconfig.conf to load your users from the database. See the section called “Dynamic Realtime” for more information about setting up
certain module configuration options in the database, and the section called “Static Realtime” for details on loading the rest of the
configuration file.
Now connect to your Asterisk console and
unload then reload the app_voicemail.so module:
*CLI>module unload app_voicemail.so== Unregistered application 'VoiceMail' == Unregistered application 'VoiceMailMain' == Unregistered application 'MailboxExists' == Unregistered application 'VMAuthenticate'*CLI>module load app_voicemail.soLoaded /usr/lib/asterisk/modules/app_voicemail.so => (Comedian Mail (Voicemail System)) == Registered application 'VoiceMail' == Registered application 'VoiceMailMain' == Registered application 'MailboxExists' == Registered application 'VMAuthenticate' == Parsing '/etc/asterisk/voicemail.conf': Found
Then verify that your new mailbox loaded successfully:
*CLI>voicemail show users for defaultContext Mbox User Zone NewMsg default 1000 J.P. Wiser 0
Testing ODBC Voicemail
Let’s create some simple dialplan logic to leave and retrieve some voicemail from our test voicemail box. You can use the simple dialplan logic that follows (or, of course, any voicemail delivery and retrieval functionality you defined earlier in this book):
[odbc_vm_test] exten => 100,1,VoiceMail(1000@default) ; leave a voicemail exten => 200,1,VoiceMailMain(1000@default) ; retrieve a voicemail
Once you’ve updated your extensions.conf file, be sure to reload the
dialplan:
*CLI>dialplan reload
You can either
include the odbc_vm_test context into a context accessible
by an existing user, or create a separate user to test with. If you wish
to do the latter, you could define a new SIP user in sip.conf like so (this will work assuming the
phone is on the local LAN):
[odbc_test_user] type=friend secret=supersecret context=odbc_vm_test host=dynamic qualify=yes disallow=all allow=ulaw allow=gsm
Warning
One of the ways that unsavory folks get into systems is via test users that are not immediately removed from the system after testing. Whenever you’re utilizing a test extension, you should be doing it on a system that is removed from the Internet, or at the very least, place it into a context that does not have access to outbound dialing and has a strong password.
Don’t forget to reload the SIP module:
*CLI>module reload chan_sip.so
And verify that the SIP user exists:
*CLI>sip show users like odbc_test_userUsername Secret Accountcode Def.Context ACL NAT odbc_test_user supersecret odbc_vm_test No RFC3581
Then configure your phone or client with
the username odbc_test_user and password
<supersecret>, and place a call to extension
100 to leave a voicemail. If
successful, you should see something like:
-- Executing VoiceMail("SIP/odbc_test_user-10228cac", "1000@default") in new
stack
-- Playing 'vm-intro' (language 'en')
-- Playing 'beep' (language 'en')
-- Recording the message
-- x=0, open writing: /var/spool/asterisk/voicemail/default/1000/tmp/dlZunm
format: wav49, 0x101f6534
-- User ended message by pressing #
-- Playing 'auth-thankyou' (language 'en')
== Parsing '/var/spool/asterisk/voicemail/default/1000/INBOX/msg0000.txt': FoundTip
At this point you can check the database to verify that your data was successfully written. See the upcoming sections for more information.
Now that you’ve confirmed everything was stored in the database
correctly, you can try listening to it via the VoiceMailMain() application by dialing
extension 200:
*CLI>
-- Executing VoiceMailMain("SIP/odbc_test_user-10228cac",
"1000@default") in new stack
-- Playing 'vm-password' (language 'en')
-- Playing 'vm-youhave' (language 'en')
-- Playing 'digits/1' (language 'en')
-- Playing 'vm-INBOX' (language 'en')
-- Playing 'vm-message' (language 'en')
-- Playing 'vm-onefor' (language 'en')
-- Playing 'vm-INBOX' (language 'en')
-- Playing 'vm-messages' (language 'en')
-- Playing 'vm-opts' (language 'en')
-- Playing 'vm-first' (language 'en')
-- Playing 'vm-message' (language 'en')
== Parsing '/var/spool/asterisk/voicemail/default/1000/INBOX/msg0000.txt': FoundVerifying binary data stored in PostgreSQL
To make sure the recording really did make it into the
database, use the psql
application:
$psql -h localhost -U asterisk asteriskPassword:
then run a SELECT statement
to verify that you have some data in the voicemessages table:
localhost=#SELECT uniqueid,dir,callerid,mailboxcontext,recording FROM voicemessages;uniqueid | dir | callerid ---------+--------------------------------------------------+-------------- 1 | /var/spool/asterisk/voicemail/default/1000/INBOX | +18005551212 | mailboxcontext | recording | +----------------+-----------+ | default | 47395 | (1 row)
If the recording was placed in the database, you should get a
row back. You’ll notice that the recording column contains a number (which
will most certainly be different from that listed here), which is
really the object ID of the large object stored in a system table. You
can verify that the large object exists in this system table with the
lo_list command:
localhost=#\lo_listLarge objects ID | Description -------+------------- 47395 | (1 row)
What you’re verifying is that the object ID in the voicemessages table matches that listed in
the large object system table. You can also pull the data out of the
database and store it to the hard drive:
localhost=#\lo_export 47395 /tmp/voicemail-47395.wavlo_export
Then verify the audio with your favorite audio application, such as play:
$play /tmp/voicemail-47395.wavInput Filename : /tmp/voicemail-47395.wav Sample Size : 8-bits Sample Encoding: wav Channels : 1 Sample Rate : 8000 Time: 00:06.22 [00:00.00] of 00:00.00 ( 0.0%) Output Buffer: 298.36K Done.
Verifying binary data stored in MySQL
To verify that your data is being written correctly, you
can use the mysql application to
log into your database and export the voicemail recording to a
file:
$mysql -u asterisk -p asteriskEnter password:
Once logged into the database, you can use a SELECT statement to dump the contents of the
recording to a file. First, though, make sure you have at least a
single recording in your voicemessages table:
mysql>SELECT uniqueid, msgnum, callerid, mailboxuser, mailboxcontext, `read`->FROM voicemessages;+----------+--------+------------------------------+------------- | uniqueid | msgnum | callerid | mailboxuser +----------+--------+------------------------------+------------- | 1 | 0 | "Leif Madsen" <100> | 100 | 2 | 1 | "Leif Madsen" <100> | 100 | 3 | 2 | "Leif Madsen" <100> | 100 | 5 | 0 | "Julie Bryant" <12565551111> | 100 +----------+--------+------------------------------+------------- +----------------+------+ | mailboxcontext | read | +----------------+------+ | shifteight.org | 0 | | shifteight.org | 0 | | shifteight.org | 0 | | default | 0 | +----------------+------+
Tip
You can also add the recording column to the SELECT statement, but you’ll end up with a
lot of gibberish on your screen.
Having verified that you have data in
your voicemessages table, you can
export one of the recordings and play it back from the console.
mysql>SELECT recording FROM voicemessages WHERE uniqueid = '5'->DUMPFILE '/tmp/voicemail_recording.wav';
Note
The user you’re exporting data with
needs to have the FILE
permission in MySQL, which means it must have been granted
ALL access. If you did not
grant ALL privileges to the
asterisk user, you will need to utilize the
root user for file export.
Now exit the MySQL console, and use the play application from the console (assuming you have speakers and a sound card configured on your Asterisk system, which you might if you are going to use it for overhead paging), or copy the file to another system and listen to it there:
$play /tmp/voicemail_recording.wavvoicemail_recording.wav: File Size: 7.28k Bit Rate: 13.1k Encoding: GSM Channels: 1 @ 16-bit Samplerate: 8000Hz Replaygain: off Duration: 00:00:04.44 In:100% 00:00:04.44 [00:00:00.00] Out:35.5k [ | ] Hd:4.4 Clip:0 Done.
Conclusion
In this chapter, we learned about several areas where Asterisk can integrate with a relational database. This is useful for systems where you need to start scaling by clustering multiple Asterisk boxes working with the same centralized information, or when you want to start building external applications to modify information without requiring a reload of the system (i.e., not requiring the modification of flat files).
[139] On a large, busy system you will want to install the database on a completely separate box from your Asterisk system.
[140] If you installed on Ubuntu, you will have already set the root password. You will have to enter that
password while executing the script, at which point it will say
you’ve already set a root
password, so you don’t need to change it.
[141] Yes, this is excessively verbose. The only entries you really
need are Driver, Database, and Servername. Even the UserName and Password are specified elsewhere, as
you’ll see later (although these are required when testing, as in
the section called “Validating the ODBC Connector”).
[142] This was actually an issue one of the authors had while
working on this book, and the flag column was found by looking at the
statement logging during PostgreSQL testing.
[143] This system is unfortunately no longer in service. Thus, any changes have been made for the sake of simplicity, not to conceal the business for which it was designed.
[144] We’re using the IF() SQL
function to make sure we return a value of 0 or 1.
This works on MySQL 5.1 or later. If it does not work on your SQL
installation, you could also check the returned result in the dialplan
using the IF() function
there.
[145] pgcluster appears to be a dead project, and Postgres-R appears to be in its infancy, so there may currently be no good solution for master-master replication using PostgreSQL.
[146] There are several tutorials on the Web describing how to set up replication with MySQL.
[147] And if you don’t know what a Dagwood is, that’s what Wikipedia is for. I am not that old.
[148] Also see Multirow Functionality with func_odbc for more information and examples of parsing multiple rows returned from the database.
[149] The musiconhold.conf file
can also be loaded via dynamic realtime, but we’re using it
statically as it’s a simple file that makes a good example.






View 1 comment



