CouchDb as FreeSwitch provisioning database

I had the idea of trying to use CouchDb as provisioning database for FreeSwitch, and started with the easiest component, the dialplan. Here’s a short overview of how I did.

FreeSwitch

I enabled mod_xml_curl module (in modules.conf.xml) and configured the URL for yaws in xml_curl.conf.xml:

<param name="gateway-url" value="https://couchdb-proxy.example.com:8443/dialplan.yaws" bindings="dialplan"/>

A restart of FreeSwitch is required (or use "load mod_xml_curl" at the CLI).

Installing ecouch in Yaws

The key to the process is a proxy between the POST format used by xml_curl and the REST format used by CouchDb. I decided to write this proxy using erlang; it doesn't have to. In my case the proxy is a yaws modapp; since I was going to be using ecouch to access CouchDb from within my erlang code, I first added the following line in yaws.conf in order to activate mod_ecouch, a small erlang module I created which starts inets and ecouch:

runmod = mod_ecouch

mod_ecouch itself is very simple:

-module(mod_ecouch).
-author('stephane@shimaore.net').

-export([start/0,stop/0]).

start() ->
  application:start(inets),
  application:start(ecouch).

stop() ->
  application:stop(ecouch),
  application:stop(inets).

Of course ecouch has to be installed (and compiled) in the default erlang library path first; on my Debian system that was /usr/lib/erlang/lib. Note: I used rfc4627.erl from Lshift to replace the incomplete version found in the original mod_ecouch.

Erlang ecouch Proxy for FreeSwitch

Then in the yaws root directory I created the following yaws script which will take a parameterized query from FreeSwitch's mod_xml_curl and transform it into a query for CouchDb. This script is the dialplan.yaws script I referenced in the configuration for mod_xml_curl earlier. To keep things very generic, the script only looks at the Hunt-Context and Hunt-Destination-Number fields provided by FreeSwitch and use them to gather a XML extension from CouchDb. If more specific routing needs to be done (for example, Caller-ID-based routing), it can be done inside the XML code generated by CouchDb (more on this later).

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml">
  <section name="dialplan" description="dialplan reply">
<erl>

out(A) ->

  Timeout = 500,

  %% We accept GET and POST (FS uses POST)
  case Q1 = queryvar(A,"Hunt-Context") of
    {ok, Context} ->
      {ok, Number } = queryvar(A,"Hunt-Destination-Number");
    _ ->
      {ok, Context} = postvar(A,"Hunt-Context"),
      {ok, Number } = postvar(A,"Hunt-Destination-Number")
  end,

  %% Query CouchDb
  Key = yaws_api:url_encode("\"" ++ Number ++ "@" ++ Context ++ "\""),

  Requestor = self(),

  spawn(fun() -> 
    Requestor ! ecouch:doc_get("dialplan", "_view/freeswitch/xml?key=" ++ Key)
  end),

  receive
    {ok, Json } ->
      %% Find the rows in the JSON reply
      { ok, Rows } = rfc4627:get_field(Json,"rows"),
      %% Concatenate the JSON "value" field in all the rows.
      Result = lists:flatten(lists:map(fun(Row) -> { ok, Entry } = rfc4627:get_field(Row,"value"), binary_to_list(Entry) end,Rows)),
      case Result of
        [] ->
          [{status,404}]; %% No results found, ask FS to fallback to file-based XML
        _ -> 
          {content,"text/xml",Result}
      end;
    _ ->
      [{status,503}]  %% Service Unavailable
  after Timeout ->
      [{status,504}]  %% Gateway Timeout
  end.

</erl>
  </section>
</document>

CouchDb records

In CouchDb I create records with context, extension (for key matching), and local_context, local_extension for the destinaton (for example -- the main idea is that the script in CouchDb is where the XML formatting takes place, so that the database is self-contained and the proxy code (above) can be kept very generic).

CouchDb View

In CouchDb I created a view called "xml" in the "freeswitch" design document which looks like this:

function(doc) {

  var xml_header =
    "<context name=\""+doc.context+"\">" +
    "<extension name=\""+doc.extension+"\">" +
    "<condition field=\"destination_number\" expression=\"^("+doc.extension+")$\">";

  var xml_footer =
    "</condition></extension></context>";

  var key = doc.extension+'@'+doc.context;

  if(doc.local_extension && doc.local_context )
  {
    var xml =
      "<action application=\"transfer\" data=\""+doc.local_extension+" XML "+doc.local_context+"\"/>";
    emit(key,xml_header+xml+xml_footer);
    return;
  }
}

This view takes regular records and map them to XML snippets that the yaws script will then feed back to FreeSwitch. As I mentioned, the script is where the flexibility of CouchDb can be harnessed to create different profiles based on different fields found in the database during the map process. This is also where the generic (destination-only) record selection process done by the erlang proxy code in dialplan.yaws can be refined.

Update: This has been in production for a while and works as advertised. posted: 2009-01-16 22:26 tags: CouchDb, erlang, FreeSwitch, mod_xml_curl, yaws

Written on January 16, 2009