Monday, April 21, 2008

Asterisk Realtime Architecture

Like any machine tinkered with heavily over time, Asterisk has a lot of exposed configuration points in a lot of places, and it can be hard to know how or why what you want to do isn't working because you neglected to set some variable that became necessary since the last time the module was documented.

I've fallen into this situation for the past couple days, and having finally found the right thing to tweak, let me write down how to use ODBC to access Asterisk's configuration files, including the dial plan, from a real-time database.

First thing: go read the official book by O'Reilly, and read the chapter on ARA.

Once you have downloaded Asterisk, as we did here, run ./configure, then make menuselect and ensure res_odbc, res_config_odbc, and all *_realtime modules are set
to be built. If they have a 'XXX' beside their name, it means that you need to install some dependencies. Search your package installer for "ODBC" and a database that supplies an ODBC driver, such as PostgreSQL or MySQL. You may also need the "-devel" version of those packages.

To make the tutorial complete, we'll pick a DB for the examples, so we choose PostgreSQL.

1. Setting Up the Database

As the DB's (not the system's) root user create a new role and database for Asterisk to use:
createuser -SRdP asterisk-role
createdb --owner=asterisk-role asterisk-db

Then restart the database.

2. Setting Up ODBC

Ensure that /etc/odbcinst.ini exists and contains something like the following:
[PostgreSQL]
Description = ODBC for PostgreSQL
Driver = /usr/lib/libodbcpsql.so
Setup = /usr/lib/libodbcpsqlS.so
FileUsage = 1

Ensure that /etc/odbcinst.ini exists and contains something like the following:
[asterisk-postgres-connector]
Description = PostgreSQL connection to 'asterisk' database
Driver = PostgreSQL
Database = asterisk-db
Servername = localhost
UserName = asterisk-role
Password = 123456

Then verify that the connection works using the following command:
echo "select 1" | isql -v asterisk-postgres-connector

3. Setting Up a Basic Asterisk Configuration

extconfig.conf
[settings]
sipusers => odbc,asterisk-postgres-connector,sip_table
sippeers => odbc,asterisk-postgres-connector,sip_table
extensions => odbc,asterisk-postgres-connector,extensions_table

extensions.conf
[general]
[default-sip]
switch => Realtime/@

modules.conf
[modules]
autoload=yes
preload => res_odbc.so
preload => res_config_odbc.so

res_odbc.conf
[ENV]

[asterisk-postgres-connector]
enabled => yes
dsn => asterisk-postgres-connector
username => asterisk-role
password => 123456
pre-connect => yes

sip.conf
[general]

4. Setting up Dialplan and Sip User/Peer Tables

This is perhaps the most mysterious point. The ARA requires a number of columns to be set within the table, and if they aren't it will silently fail. The SIP table in particular. Nearest I can tell, the only source of documentation is here.

Run the following SQL code as the asterisk-role:

CREATE TABLE extensions_table
(
"id" serial,
"context" varchar(20) NOT NULL default '',
"exten" varchar(36) NOT NULL default '',
"priority" smallint NOT NULL default '0',
"app" varchar(20) NOT NULL default '',
"appdata" varchar(128) NOT NULL default '',

PRIMARY KEY (id)
) WITHOUT OIDS;

CREATE TABLE sip_table
(
"id" serial,
"name" varchar(80) NOT NULL default '',
"host" varchar(31) NOT NULL default 'dynamic',
"nat" varchar(5) NOT NULL default 'route',
"type" varchar(6) check (type in ('user','peer','friend')) NOT NULL default 'friend',
"accountcode" varchar(20) default NULL,
"amaflags" varchar(13) default NULL,
"callgroup" varchar(10) default NULL,
"callerid" varchar(80) default NULL,
"call-limit" int NOT NULL default '0',
"cancallforward" char(3) default 'no',
"canreinvite" char(3) default 'no',
"context" varchar(80) default 'default-sip',
"defaultip" varchar(15) default NULL,
"dtmfmode" varchar(7) default NULL,
"fromuser" varchar(80) default NULL,
"fromdomain" varchar(80) default NULL,
"insecure" varchar(4) default NULL,
"language" char(2) default NULL,
"mailbox" varchar(50) default NULL,
"md5secret" varchar(80) default NULL,
"deny" varchar(95) default NULL,
"permit" varchar(95) default NULL,
"mask" varchar(95) default NULL,
"musiconhold" varchar(100) default NULL,
"pickupgroup" varchar(10) default NULL,
"qualify" char(3) default NULL,
"regexten" varchar(80) default NULL,
"restrictcid" char(3) default NULL,
"rtptimeout" char(3) default NULL,
"rtpholdtimeout" char(3) default NULL,
"secret" varchar(80) default NULL,
"setvar" varchar(100) default NULL,
"disallow" varchar(100) default '',
"allow" varchar(100) default '',
"fullcontact" varchar(80) NOT NULL default '',
"ipaddr" varchar(15) NOT NULL default '',
"port" smallint NOT NULL default '0',
"regserver" varchar(100) default NULL,
"regseconds" int NOT NULL default '0',
"username" varchar(80) NOT NULL default '',
"delay" int NOT NULL default '0',
"sortorder" int NOT NULL default '1',
PRIMARY KEY (id),
UNIQUE (name)
) WITHOUT OIDS;

GRANT ALL ON sip_table TO asterisk;
GRANT ALL ON extensions_table TO asterisk;

INSERT INTO extensions_table (context, exten, priority, app, appdata) VALUES ('default-sip', 'sip-extn', 1, 'Answer', '');
INSERT INTO extensions_table (context, exten, priority, app, appdata) VALUES ('default-sip', 'sip-extn', 2, 'Wait', '2');
INSERT INTO extensions_table (context, exten, priority, app, appdata) VALUES ('default-sip', 'sip-extn', 3, 'Playback', 'hello-world');
INSERT INTO extensions_table (context, exten, priority, app, appdata) VALUES ('default-sip', 'sip-extn', 4, 'Wait', '2');
INSERT INTO extensions_table (context, exten, priority, app, appdata) VALUES ('default-sip', 'sip-extn', 5, 'Hangup', '');

INSERT INTO sip_table (name, context, host, type) VALUES ('sip-user', 'default-sip', 'dynamic', 'friend');

5. Test the setup

Run Asterisk on the console, and verify the ODBC connection is working by issuing the command odbc show.

Don't bother trying to run dialplan show or similar, as this will not display what we inserted in the previous step. Instead we need to make a call to test: in your soft-phone, dial sip:sip-extn@asterisk.example.com, and you should hear a voice say "hello world".

Thursday, April 17, 2008

Working with Asterisk

Recently I have been working on some VoIP based solutions using Asterisk.

I've found it a bit hard to work on, partly because Asterisk appears to suffer from some ad-hoc growth over time, and not only is the design somewhat dated, but more importantly the documentation left around the web appears to be tied to past versions such that working with the newest versions can leave you scratching you head why what one guy did, and what you're doing don't seem to match up. Even when you manage to get the example working you're never quite sure which flag you twiddled happened to do the trick.

(Aside: there seems to be a lot of consulting business around Asterisk. Partly this is due to the inherently Enterprise-related nature of a PBX. However I have to wonder if these consultants don't have a vested interest in keeping Asterisk opaque. Certainly they'd have more clients than if the community spent time making it easier to use.)

First thing one needs to do when learning a system is to make some small examples. The easiest way to test your examples, lacking the sort of hardware one might have if they owned a real Enterprise phone system, is to use software phones. The most common kind of software phone is one that uses SIP to set up an audio stream (over RTP) for voice communication.

To obtain a working Asterisk server, download either the source, or a pre-compiled binary from your favorite location. If you're working a lot with Asterisk it makes sense to build the source yourself, since it's rather easy to build.

After you have completed the install, your /etc/asterisk/ directory should be full of a wild assortment of configuration files, each over-teeming with tweak-worth options. However just to set up a simple call, we don't need such complexity. Copy those files to a safe place, and consider the minimal files below:

modules.conf
As far as I can tell, this is all you'll ever need from modules.conf, under any situation.

[modules]
autoload=yes

This means that as Asterisk looks for .conf files (which we have mostly removed), and processes its internal and external dependencies, it will automatically load the binary files in /usr/lib/asterisk/modules.

sip.conf
This file controls the SIP input "channel", from which any session created from
sip:a-sip-user@example.com to sip:somewhere@asterisk.example.com, must pass first on its way through an Asterisk system. The call from a-sip-user is assigned a "context" which is where in the "dialplan" the call should be routed. ("user" means that the call is in-bound only, and "dynamic" means the server should look up the user's IP address dynamically)

[a-sip-user]
context=my-sip-context
type=user
host=dynamic

This means that anyone who call into Asterisk, and declares themselves to be "a-sip-user" via their SIP URI (such as sip:a-sip-user@somewhere.example.com), is given a dial plan context of "my-sip-context".

extensions.conf
This is the "dial plan", the place where the routing logic of a PBX is stored. It consists of a number of named contexts, under which a series of sequential functions are called. It will appear odd to a modern programmer, but if one considers the evolution of old-fashioned hardware based PBXs, it makes more sense.

Basically there is first an "extension", which is related to the old extension numbers you would have to dial over an old-fashioned system. However with software based PBXs, those extensions can now easily be human-readable strings.

A call is brought in to the dial plan from a channel. The only channel we are considering now is a SIP-based VoIP call. That channel attached a context to the call, and starting from the named context, the dial plan attempts to match an extension pattern to the one specified in a call.

In the case of a SIP call, where the URI is
sip:my-destination@asterisk.example.com, the extension is considered to be "my-destination".

[my-sip-context]
exten => sip-extn,1,Answer()
exten => sip-extn,n,Wait(2)
exten => sip-extn,n,Playback(hello-world)
exten => sip-extn,n,Wait(2)
exten => sip-extn,n,Hangup()

This means that when dropped in the "my-sip-context" context, Asterisk will attempt to match the extension against "sip-extn". The first action to be taken is to answer the call. The steps with priority marked "n" proceed sequentially from each other.

In the example above, the soft-phone must dial the following URI:
sip:sip-extn@asterisk.example.com as the user designated sip:a-sip-user@somewhere.example.com. Upon doing so, they should hear a female voice saying "hello world!"

If you are using ekiga as your soft-phone, go to Edit->Accounts, and then add a new one. The name can be any thing you like, however the registrar must be asterisk.example.com (that is the name or IP address of the machine running Asterisk), and the user must be a-sip-user. The password is empty because we have not required password protection in sip.conf.

Click OK and confirm that the Status indicates that it is registered with the server. If registration fails, check that the server's firewall is not blocking port 5060, and that you are not using the soft-phone from the same machine that Asterisk is running on since port numbers will conflict.

In the top-most address bar, enter the URI sip:sip-extn@asterisk.example.com and click enter. You should now hear the words "hello world!"

The version of Asterisk I used was 1.6.

Sunday, April 13, 2008

Thursday, April 10, 2008

How do you use your shell?

[ryanm@3Di0021d ~]$ history | awk '{a[$2]++ } END{for(i in a){print a[i] " " i}}'|sort -rn|head
177 make
144 ll
96 git
77 cd
59 grep
59 gvim
40 cp
38 git-svn
32 ./secondlife
30 svn