9780596517342
asterisk-DB.html

Chapter 16. Relational Database Integration

Few things are harder to put up with than the annoyance of a good example.

--Mark Twain

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:

$ sudo yum install -y postgresql-server
Install       3 Package(s)
Upgrade       0 Package(s)

Total download size: 6.9 M
Is this ok [y/N]: y

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-server
Install       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-server
Need 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 -P
Enter name of user to add: asterisk
Enter password for new user:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new user be allowed to create databases? (y/n) y
Shall the new user be allowed to create more new users? (y/n) n
CREATE 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 asterisk
CREATE DATABASE

You can set the password for the asterisk user like so:

$ psql -d template1
template1=# "ALTER USER asterisk WITH PASSWORD 'password'"
template1=# \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 asterisk 
Password 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 quit

asterisk=>

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 -p
Enter 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> exit
Bye
# mysql -u asterisk -p asterisk
Enter 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

On Ubuntu:

$ 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   = 2
Ctrl+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            = 1433

In 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 show

ODBC 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:

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 asterisk
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)
)
WITHOUT OIDS;

For MySQL:

$ mysql -u asterisk -p asterisk
Enter 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 nameColumn type
idSerial, auto-incrementing
extensionInteger
first_nameText
last_nameText
cid_nameText
cid_numberVarchar 10
pinInteger
contextText
statusBoolean, default false
locationText

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

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

Column nameColumn typeDescription
idSerial, auto-incrementingAn auto-incrementing unique value for each row in the table.
cat_metricIntegerThe weight of the category within the file. A lower metric means it appears higher in the file (see the sidebar ).
var_metricIntegerThe weight of an item within a category. A lower metric means it appears higher in the list (see the sidebar ). This is useful for things like codec order in sip.conf, or iax.conf where you want disallow=all to appear first (metric of 0), followed by allow=ulaw (metric of 1), then allow=gsm (metric of 2).
filenameVarchar 128The filename the module would normally read from the hard drive of your system (e.g., musiconhold.conf, sip.conf, iax.conf, etc.).
categoryVarchar 128The section name within the file, such as [general]. Do not include the square brackets around the name when saving to the database.
var_nameVarchar 128The option on the left side of the equals sign (e.g., disallow is the var_name in disallow=all).
var_valVarchar 128The value of an option on the right side of the equals sign (e.g., all is the var_val in disallow=all).
commentedIntegerAny value other than 0 will evaluate as if it were prefixed with a semicolon in the flat file (commented out).

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 classes
Class: 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 nameColumn type
typeVarchar 6
nameVarchar 128
secretVarchar 128
contextVarchar 128
hostVarchar 128
ipaddrVarchar 128
portVarchar 5
regsecondsBigint
defaultuserVarchar 128
fullcontactVarchar 128
regserverVarchar 128
useragentVarchar 128
lastmsInteger

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 nameValue
typefriend
name0000FFFF0008
defaultuser0000FFFF0008
hostdynamic
secretwelcome
contextLocalSets

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 optionDescription
rtcachefriendsCaches 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.
rtsavesysnameWhen 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.
rtupdateSends 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.
rtautoclearAutomatically 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.
ignoreregexpireWhen 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 status

Call Detail Record (CDR) settings
----------------------------------
  Logging:                    Enabled
  Mode:                       Simple
  Log unanswered calls:       No

* Registered Backends
  -------------------
    cdr-syslog
    Adaptive ODBC
    cdr-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 asterisk
Password:

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 nameColumn type
uniqueidSerial, primary key
dirVarchar 80
msgnumInteger
recordingBLOB (Binary Large OBject)
contextVarchar 80
macrocontextVarchar 80
calleridVarchar 40
origtimeVarchar 40
durationVarchar 20
mailboxuserVarchar 80
mailboxcontextVarchar 80
labelVarchar 30
readBoolean, default false[a]
flagVarchar 10

[a] read is a reserved word in both MySQL and PostgreSQL (and likely other databases), which means you need to escape the column name when you create it. In MySQL this is done with backticks (`) around the word read when you create the table, and in PostgreSQL with double quotes ("). In MS SQL you would use square brackets, e.g., [read].


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.so
 Loaded /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 default
Context    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_user
Username         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': Found

Tip

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': Found

Verifying 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 asterisk
Password:

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_list
    Large 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.wav
lo_export

Then verify the audio with your favorite audio application, such as play:

$ play /tmp/voicemail-47395.wav

Input 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 asterisk
Enter 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.wav 

voicemail_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.

Site last updated on: September 27, 2011 at 10:30:41 AM PDT
Cover for Asterisk: The Definitive Guide

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        View 1 comment

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        1. David Van Ginneken – Posted Jan. 11, 2011

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          In multirow_example, I believe the loop will ignore the last record. I think this: same => n,While($[${COUNTER} < ${ODBCROWS}]) Should be: same => n,While($[${COUNTER} < $[${ODBCROWS}+1]])

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          The author has indicated that the issue raised in this comment has been resolved.

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            View 1 comment

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            1. thp4 – Posted Jan. 8, 2011

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              Isn't that 'respectively' not respectfully ?

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              The author has indicated that the issue raised in this comment has been resolved.