confdir='/etc/asterisk/lcdialsh/' # location of lcdc.conf - end with '/' ratesdir="${confdir}rates/" # location of p_ and c_ files setcmd='SetVar' # 'SetVar' in Asterisk < 1.1, 'Set' above version='0.1a' # version of this program # Least-Cost Dialplan Compiler # Automatically builds a "dialplan fragment" file to be #included in # extensions.com and used through a single macro call named after the # command line argument of this script. The macro's name, # called "root context", is also used as prefix in all the dialplan # variables in order to reduce the risk of namespace collision; and # also determines the name of the configuration file (see below). # This allows multiple "least-cost sub-dialplans" to coexist at the # same time, e.g. for use by different callers. # # The compiler uses as primary input: # - a configuration file with a format derived from the one used by LCDial.sh # - the same ratefiles used by LCDial.sh # - the "p_" and "c_" files introduced with the version 0.8 of LCDial.sh # # No other dependencies exist from LCDial.sh, and in fact the # output of the compiler represents a static, non-AGI and therefore much # faster alternative to LCDial.sh (although a few features of the latter # are still missing here, and the semantic of the dialling and postdialling # fields is different -- see below). # # If the "root context" is, say, 'lcd', the configuration file will # be assumed to be named "lcd.conf", located in the $confdir directory # (configurable at the beginning of this script); the dialplan fragment # file produced from it will be named lcd.inc, and placed in the same # directory. To use it, just replace in the dialplan calls to # AGI(LCDialsh,....) with "Macro(lcd,...)" passing the the same first # five parameters. For instance, # # exten => _00X.,1,AGI(LCDial.sh,${EXTEN:2},,,,3) # # will become: # # exten => _00X.,1,Macro(lcd,${EXTEN:2},,,,3) # # At the end of the dialplan, in extensions.conf, add the line: # # #include lcdialsh/lcd.inc # # It will also be necessary to define in extensions.conf all the # "pre-call" macros referenced in the configuration file # (see below), e.g.: # # [macro-announce-cost] # ; divide by 1,000 to get cost in US cents # exten => s,1,SetVar(tmp=000${ARG2}) # exten => s,2,SayNumber(${tmp:000:$[${LEN(${tmp})} - 3]}) # exten => s,3,SayAlpha(US) # exten => s,4,Background(cents) # exten => s,5,Background(pls-wait-connect-call) # exten => s,6,Goto(9999) # # [macro-announce-cost-euro] # ; divide by 1,000 to get cost in cents # exten => s,1,SetVar(tmp=000${ARG2}) # exten => s,2,SayNumber(${tmp:0:$[${LEN(${tmp})} - 3]}) # exten => s,3,Background(euro) # exten => s,4,Background(cents) # exten => s,5,Background(pls-wait-connect-call) # exten => s,6,Goto(9999) # # Parameters to pass from the dialplan to the least-cost dial macro: # # ${ARG1} Dial() called number. # ${ARG2} Dial() timeout (or unset) # ${ARG3} Dial() options string (or unset) # ${ARG4} Dial() URL string (or unset) # ${ARG5} max acceptable cost in millicents/min (or empty, i.e. no limit) # ${ARG6} mute: if NOT empty, skip the pre-call macro for each provider. # NOTE: this is important when using the # least-cost dial macro from a callfile: besides being useless # because nobody listens to its announcements, it's harmful because # Asterisks assumes that the call was answered as soon as the # channel passes sound... # # Format of the configuration file # # To preserve some backward compatibility with LCDial.sh.conf, the # fields in lcdc.conf are interpreted as follows: # # 1. Provider's name (as in LCDial.sh) # 2. Provider's ratefile. The rules of the game, here, are: # - if empty (i.e., ""), then rate rules are built "on the fly" # from the "c_" files referenced by the provider's "p_" file. # If such "p_" file does not exist, a warning is printed and # that provider is ignored. # - if not empty (pointing to a ratefile), then: # - if the ratefile is newer than the "p_" file of that # provider and all the "c_" files referenced by it, or # if such "p_" file does not exist, or if the ratefile is # not in the same directory as the p_file and with the # same name as the provider, we assume that the ratefile # is either up-to-date or was manually edited: so it is used # as it is; # - else, if a "p_$provname" file exists, a new ratefile is # generated with the specified name, and then used. # - else, if if a "p_$(basename $provpathratefile)" file exists, # a new ratefile is generated with that name, and then used. # It may sound complicated, but it'll generally do "the right thing" # according to the user's expectations. # 3. Dialing string. A string usually containing ${EXTEN} and # acceptable as first parameter of an Asterisk Dial() command. # 4. Extra options. A string that will be appended to the string # of options characters passed as third parameter of the macro # call. A typical use is a postdialling string, starting by # "D(", containing ${EXTEN} mixed with extra digits and metadigits, # and ending in ")". # 5. Own country code. Some providers require a dialling scheme whereby # their own country code is removed from the head of the dialled # number, and (sometimes) replaced by a "LD prefix" # 6. LD prefix: if the "Own country code" field is not empty, when # the destination starts by it it will be removed and replaced by # the content of this field (possibly empty). # 7. IDD prefix: if the "Own country code" field is not empty, when # the destination DOES NOT start by it it will be prefixed by the # content of this field (possibly empty). Typically, US-based # PSTN termination providers such as Teliax or Voipjet require: # Own country code: 1 LD prefix: 1 IDD prefix: 011 # EU-based PSTN termination providers (VoipBuster etc.) require: # Own country code: "" LD prefix: "" IDD prefix: 00 # Free providers for toll-free destinations, such as those # returned by ENUM, usually require: # Own country code: "" LD prefix: "" IDD prefix: "" # 8. Pre-call macro. If this field is not empty, immediately before # trying to deliver the call through this provider, a macro with # this name will be invoked, receiving as first argument the # dialled number (${EXTEN}) and as second argument the cost # of delivery in millicents per minute. The macro will have to be # separately provided in the main dialplan by the user, typically # to announce the cost per minute of the call being attempted. # # IMPORTANT!! the fields now MUST be separated by a least one tab, *NOT* # just whitespaces. This is necessary because the various fields # can now contain embedded spaces. Empty fields are acceptable # when this makes sense, and must be indicated by a "" placeholder. # # Returned values: # Calls to Macro(lcd,...) do not return if the call can be established # of fails for BUSY or NOANSWER. They do return, allowing the dialplan # to try some other route, if: # - The cost limit is too low for the available routes. In that case, # the variable ${DIALSTATUS} is set to an empty string. # - The call fails with Dial() returning CONGESTION of CHANUNAVAIL, # and in those cases the value of ${DIALSTATUS} is the same as set # by Dial(). # # ToDo: # - Support for time-of-day rates (unlikely to ever be added...) lf=" " tab=$(echo '_' | tr '_' '\t') IFS=" $lf$tab" curdir=$(pwd) verbose_log() { if [ $verbose ]; then echo "$*" fi } if [ _"$1" = _"-v" ]; then verbose=1 shift else verbose="" fi progname="$(basename $0)" if [ _"$1" = _ ]; then echo "$progname - v.$version" echo "usage: $progname [-v] rootcontext" exit fi rootcontext="$1" conffile="${confdir}$rootcontext.conf" # the conf file itself if [ ! -e "$conffile" ]; then echo "*** Configuration file $conffile does not exist, aborting." exit 1 fi includefile="$rootcontext.inc" tmpincludefile="/tmp/$includefile.$$" usergroup=$(ls -ld ${confdir} | sed 's/[^[:space:]]\+[[:space:]]\+[^[:space:]]\+[[:space:]]\+\([^[:space:]]\+\)[[:space:]]\+\([^[:space:]]\+\).*/\1.\2/') cd "$ratesdir" rm -f $tmpincludefile 2>/dev/null comment() { echo >>"$tmpincludefile" "; $1" } comment "Dialplan fragment created by $(basename $0) v.$version" comment "Invoke with e.g.: exten => _0X.,1,Macro($rootcontext,\${EXTEN:1},3)" comment "" parsefield() { local field echo $1 | sed -e 's/^ *//;s/ *$//;s/^\"\"$//' } providerslist="" # iterate on all providers mentioned in LCDial.sh : IFS="$lf" # remove empty lines pr lines where the first non-blank is '#' or ';' (i.e., comments) # NOTE: '#' or ';' in the middle of the line are NOT considered comment starters for provline in $(sed <$conffile "s/^[[:space:]]*[#;].*//;s/^[[:space:]]*$//;/^$/d"); do IFS="$tab" set -- $provline IFS=" $lf$tab" prov=$(parsefield $1) ratef=$(parsefield $2) dialc=$(parsefield $3) extraoptc=$(parsefield $4) ownccode=$(parsefield $5) ldprefix=$(parsefield $6) iddprefix=$(parsefield $7) precallmacro=$(parsefield $8) #echo "Conf>>$prov:$ratef:$dialc:$extraoptc:$ownccode:$ldprefix:$iddprefix:$precallmacro<<" ########## provname=$(echo "$prov" | sed 's/[>].*//') # remove any (now useless) ">nn" suffix verbose_log "processing $provname..." if [ _"$dialc" = _ ]; then echo "*** WARNING: missing dialing rule for provider ${provname} in $conffile" continue fi if [ _"$ownccode" != _ -a _"$iddprefix" = _ ]; then echo "*** WARNING: non-empty own c. code $ownccode but empty iddprefix for provider ${provname} in $conffile" continue fi eval ${provname}_dialcmd="\$dialc" eval ${provname}_extraoptcmd="\$extraoptc" eval ${provname}_ownccode="\$ownccode" eval ${provname}_ldprefix="\$ldprefix" eval ${provname}_iddprefix="\$iddprefix" eval ${provname}_precallmacro="\$precallmacro" if [ _"$ratef" = _ ]; then # if conf file specifies NO ratefile # if provider $provname has a p_ file in $ratesdir, then use ratefile components in its c_ files; if [ -f p_$provname ]; then ratefileslist="$(sed /dev/null -t $(sed 2>/dev/null 's/^/c_/' "$provp_file") "$provp_file" "$basenameratefile" | sed q) if [ "$newest" != "$basenameratefile" ]; then verbose_log "$basenameratefile out of date, rebuilding..." rm -f "$basenameratefile" for fc in $(cat "$provp_file"); do echo >>"$basenameratefile" "; $fc" sed >>"$basenameratefile" "s/[#;].*//;s/[[:space:]]\+\$//;s/[[:space:]]\+/$tab/;/^\$/d" < "c_$fc" done verbose_log "...done." fi fi # so do we have the required ratefile now? if [ -f "$provpathratefile" ]; then ratefileslist="$provpathratefile" else echo "*** WARNING: the specified ratefile $provpathratefile can't be found or built: $provname ignored!" continue; fi fi # here, hopefully, ratefileslist has the list of ratefiles for the provider $provname echo >>"$tmpincludefile" "[macro-$rootcontext-$provname]" echo >>"$tmpincludefile" "exten => s,1,Goto(\${ARG1},1)" echo >>"$tmpincludefile" "exten => _XXX.,1,$setcmd($rootcontext-c=!)" #exten => _XXX.,1,GoTo(999)' for fc in $ratefileslist; do verbose_log " adding rules for $fc..." comment "$fc" sed <"$fc" -e "s/[#;].*//;s/[[:space:]]\+\$//;s/[[:space:]]\+/$tab/;/^\$/d" \ | while read prefix cost; do if [ _"$prefix" = _"-" ]; then prefix="" fi if [ _"$cost" = _"!" ]; then # the double X avoids conflicts in the same provider-subcontext with non-exclusion # rules, and lets the latter take precedence due to sorting criteria... # The GoTo(999) ensures return, which occurs if no such step exists in the # macro, NOT EVEN FOR OTHER MATCHING PATTERNS. Otherwise, the execution will # go to that step number for the next matching extension. echo >>"$tmpincludefile" "exten => _${prefix}XX.,1,$setcmd($rootcontext-c=$cost)" # (unnecessary:) echo >>"$tmpincludefile" "exten => _${prefix}XX.,2,GoTo(999)" else echo >>"$tmpincludefile" "exten => _${prefix}X.,1,$setcmd($rootcontext-c=$cost)" # (unnecessary:) echo >>"$tmpincludefile" "exten => _${prefix}X.,2,GoTo(999)" fi done done providerslist="$providerslist $provname" done cd $curdir verbose_log " creating root context [macro-$rootcontext]..." # Now let's create the root context rules to: # 1. Call each provider's macro, to determine the cost of the best match # 2. Sort in reverse cost order for this extension # 3. Dial providers in sorted order, breaking away if status different # from CHANUNAVAIL or CONGESTION emit() { echo >>"$tmpincludefile" "exten => _X.,$rootpri,$1" rootpri=$(expr $rootpri + 1) } comment "" comment "Least Cost Dialling macro. Accepts the following parameters:" comment "\${ARG1} Dial() called number" comment "\${ARG2} Dial() timeout (or unset)" comment "\${ARG3} Dial() options string (or unset)" comment "\${ARG4} Dial() URL string (or unset)" comment "\${ARG5} max cost in millicents (or unset)" comment "\${ARG6} mute: if not empty, skip the pre-call macro before dialling" comment "" echo >>"$tmpincludefile" "[macro-$rootcontext]" echo >>"$tmpincludefile" "exten => s,1,$setcmd($rootcontext-dialtimeout=\${ARG2})" echo >>"$tmpincludefile" "exten => s,2,$setcmd($rootcontext-dialoptions=\${ARG3})" echo >>"$tmpincludefile" "exten => s,3,$setcmd($rootcontext-dialURL=\${ARG4})" echo >>"$tmpincludefile" "exten => s,4,$setcmd($rootcontext-costlimit=10000000)" # default $100/min echo >>"$tmpincludefile" "exten => s,5,GotoIf(\$[\${LEN(\${ARG5})} = 0]?\${ARG1},1)" # if missing, use costlimit as default echo >>"$tmpincludefile" "exten => s,6,$setcmd($rootcontext-costlimit=\${ARG5})" echo >>"$tmpincludefile" "exten => s,7,$setcmd($rootcontext-skipprecallmacro=\${ARG6})" echo >>"$tmpincludefile" "exten => s,8,GoTo(\${ARG1},1)" comment "for each provider, get the cost for the current route" comment "and retain only providers with cost below the assigned limit" # first assign the names to the "array" $rootcontext-prov1 ... $rootcontext-provn rootpri=1 emit "$setcmd($rootcontext-provnum=0)" # in the generated code, the loop is actually unwound. for provname in $providerslist; do # get the config fields for this provider eval dialcmd="\$${provname}_dialcmd" eval extraoptcmd="\$${provname}_extraoptcmd" eval ownccode="\$${provname}_ownccode" eval iddprefix="\$${provname}_iddprefix" eval ldprefix="\$${provname}_ldprefix" eval precallmacro="\$${provname}_precallmacro" # call the macro to get the cost in $rootcontext-c emit "Macro($rootcontext-$provname,\${ARG1})" # emit "NoOp($rootcontext-$provname returned cost: \${$rootcontext-c})" # if the cost returned is not acceptable, skip this provider if [ "_$ownccode" = "_" ]; then labelskipprov=$(expr $rootpri + 7) ###### else labelskipprov=$(expr $rootpri + 11) ###### fi emit "GotoIf(\$[\"\${$rootcontext-c}\" = \"\" | \${$rootcontext-c} = ! | \${$rootcontext-c} >= \${$rootcontext-costlimit}]?$labelskipprov)" emit "$setcmd($rootcontext-$provname-cost=\${$rootcontext-c})" emit "$setcmd($rootcontext-provnum=\$[\${$rootcontext-provnum} + 1])" emit "$setcmd($rootcontext-prov\${$rootcontext-provnum}=$provname)" emit "$setcmd($rootcontext-$provname-precallmacro=$precallmacro)" # Let's adjust dial and extraopt depending on $ownccode and ${EXTEN} # at runtime, we know the value of ${EXTEN}. If it starts with ownccode, # strip it away; in that case also prefix with ldprefix (if any). # Otherwise, prefix it with iddprefix (if any). dialcmdother=$(echo $dialcmd | sed -e "s/\${EXTEN}/$iddprefix\${EXTEN}/g"); extraoptcmdother=$(echo $extraoptcmd | sed -e "s/\${EXTEN}/$iddprefix\${EXTEN}/g"); if [ "_$ownccode" != "_" ]; then # generate istructions to set dial and extraopt depending on ${EXTEN} # some ancient versions of sh don't understand ${#...} :-( ownlen=$(echo $ownccode | awk '{print length($1)}') # when ${EXTEN} starts by ownccode, remove ccode, add ldcode dialcmdown=$(echo $dialcmd | sed -e "s/\${EXTEN}/$ldprefix\${EXTEN:$ownlen}/g") # when ${EXTEN} does NOT start by ownccode, just add iddcode extraoptcmdown=$(echo $extraoptcmd | sed -e "s/\${EXTEN}/$ldprefix\${EXTEN:$ownlen}/g") labelskipownccode=$(expr $rootpri + 4) ###### emit "GotoIf(\$[\${EXTEN} : $ownccode.*]?:$labelskipownccode)" # ${EXTEN} starts by ownccode: use the LD strings emit "$setcmd($rootcontext-$provname-dial=$dialcmdown)" emit "$setcmd($rootcontext-$provname-extraopt=$extraoptcmdown)" labelskipotherccode=$(expr $rootpri + 3) ###### emit "GoTo($labelskipotherccode)" # ${EXTEN} does NOT start by ownccode: use the IDD strings fi emit "$setcmd($rootcontext-$provname-dial=$dialcmdother)" emit "$setcmd($rootcontext-$provname-extraopt=$extraoptcmdother)" done # now emit the bubble sort code (yeah, inefficient, but providers are few...) comment "bubble sort providers with cost to the current route below our limit" # if less than 2 providers, skip sort labelskipsort=$(expr $rootpri + 13) ###### emit "GotoIf(\$[\${$rootcontext-provnum} < 2]?$labelskipsort)" labeliter=$rootpri emit "$setcmd($rootcontext-swapped=0)" emit "$setcmd($rootcontext-thisprov=1)" emit "$setcmd($rootcontext-nextprov=2)" labelforloop=$rootpri labelskipswap=$(expr $rootpri + 5) ####### # compare costs. At same cost, leave the original order in config file emit "GotoIf(\$[\${$rootcontext-\${$rootcontext-prov\${$rootcontext-thisprov}}-cost} <= \${$rootcontext-\${$rootcontext-prov\${$rootcontext-nextprov}}-cost}]?$labelskipswap)" # swap this with next only because this' cost > next's, # OR it is the same but this' name alphabetically follows next's. emit "$setcmd(tmp=\${$rootcontext-prov\${$rootcontext-nextprov}})" emit "$setcmd($rootcontext-prov\${$rootcontext-nextprov}=\${$rootcontext-prov\${$rootcontext-thisprov}})" emit "$setcmd($rootcontext-prov\${$rootcontext-thisprov}=\${tmp})" emit "$setcmd($rootcontext-swapped=1)" # increment pointers emit "$setcmd($rootcontext-thisprov=\${$rootcontext-nextprov})" emit "$setcmd($rootcontext-nextprov=\$[\${$rootcontext-nextprov} + 1])" # if next still within range (1..n) then loop emit "GoToIf(\$[\${$rootcontext-nextprov} <= \${$rootcontext-provnum}]?$labelforloop)" # while swaps occur, do iterate emit "GoToIf(\$[\${$rootcontext-swapped} > 0]?$labeliter)" # Now the names are sorted by least cost, so we may dial one by one comment "try and Dial() providers in reverse cost order, till one gets through" emit "$setcmd($rootcontext-thisprov=1)" labelloopdial=$rootpri emit "GotoIf(\$[\${$rootcontext-thisprov} > \${$rootcontext-provnum}]?999)" # Call the pre-dial macro, if not null for this provider cost="\${$rootcontext-\${$rootcontext-prov\${$rootcontext-thisprov}}-cost}" precallmacro="\${$rootcontext-\${$rootcontext-prov\${$rootcontext-thisprov}}-precallmacro}" labelskipprecallmacro=$(expr $rootpri + 2) emit "GotoIf(\$[\"$precallmacro\" = \"\" | \"\${$rootcontext-skipprecallmacro}\" != \"\" ]?$labelskipprecallmacro)" emit "Macro($precallmacro,\${EXTEN},$cost)" # dial and extraopt have been already modified at runtime before the sort # plug extraopt in options NOTE: any option with the same name passed in # the macro call is NOT removed!!! extraoptcmd="$rootcontext-\${$rootcontext-prov\${$rootcontext-thisprov}}-extraopt" d1="\${$rootcontext-\${$rootcontext-prov\${$rootcontext-thisprov}}-dial}" d2="\${$rootcontext-dialtimeout}" d3="\${$rootcontext-dialoptions}\${$extraoptcmd}" d4="\${$rootcontext-dialURL}" # We have now to massage the dialing string based on both ownccode and ${EXTEN} emit "Dial($d1,$d2,$d3,$d4)" emit "GotoIf(\$[\"\${DIALSTATUS}\" = \"CONGESTION\" | \"\${DIALSTATUS}\" = \"CHANUNAVAIL\"]?:999)" emit "$setcmd($rootcontext-thisprov=\$[\${$rootcontext-thisprov} + 1])" emit "GoTo($labelloopdial)" comment "return to caller, unless last Dial() returned to get ANSWERED, NOANSWER or BUSY" mv "$tmpincludefile" "$confdir$includefile" chown "$usergroup" "$confdir$includefile" echo "Done. Remember to put at the end of extensions.conf the line:" echo "#include $confdir$includefile" echo "...and call free destinations with rules like:" echo "exten => _0X.,1,Macro($rootcontext,\${EXTEN:1})" exit