Блог ramboza

Регистрация

<< Декабрь 2010  

Пн Вт Ср Чт Пт Сб Вс
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31

cisco voip&other stuff

Теги

bgp  call manager  ccme  cdr  cisco  conference bridge  cucm  debug  dmvpn  dsp  dspfarm  dtmf  dual-dmvpn  dual-hub  e1  egp  eigrp  full-mesh  gatekeeper  gateway  gre  gwgk  hub&spoke  igp  ios  isdn  label  ldp  leaking  media termination point  mgcp  mgre  mpls  mtp  onramp offramp fax email fax2email faxto  php  pri  pvdm  python  q931  qos  route  sccp  tcl  transcoder  voip  vpn  vrf 


Дружественные ресурсы:

sysadmins.ru
sysadmins.su
certification.ru
data2money.com
qualedi.ru

Basic MPLS VPN & VRF ROUTE LEAKING

Topology* :



* The ISP zone is simplified so the MPLS-backbone to contain only 2 directly connected PE-borders. Normally, such a lab should have 2-3 more P-LSRs to participate in mpls-labeled packet forwarding for understanding purposes.

R1 (Customer-A Site 1)

R5 (CustomerA Site 2)

R2 (ISP-PE1)


R4 (ISP-PE2)

Since the VRF prefixes are only locally significant, we should label them via mpls beside exporting into ISP-BGP-AS, so that the neighbor ISP-PE could figure out how to handle the incoming traffic, i.e. direct it into the correct VRF (could be many of them, with same addressing).

Also note, that you have to run some IGP (EIGRP in this case) for NLRI exchange so that MPLS label allocation and propagation occurs correctly.

Verify (ISP-PE1):

sh ip bgp all

For address family: VPNv4 Unicast
BGP table version is 9, local router ID is 10.10.10.101
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
              r RIB-failure, S Stale
Origin codes: i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
Route Distinguisher: 1:100 (default for vrf VPN-A)
* > 172.16.1.0/30    0.0.0.0                  0         32768 ?
* >i172.16.2.0/30    10.10.10.102             0    100      0 ?
* > 192.168.1.0      172.16.1.2               0         32768 ?
* >i192.168.2.0      10.10.10.102             0    100      0 ?


sh mpls forwarding-table

Local  Outgoing    Prefix            Bytes tag  Outgoing   Next Hop
tag    tag or VC   or Tunnel Id      switched   interface
16     Pop tag     10.10.10.102/32   0          Fa1/0      10.10.10.2
17     Aggregate   172.16.1.0/30[V]  1352
18     Untagged    192.168.1.0/24[V] 570        Fa0/0      172.16.1.2

sh mpls forwarding-table vrf VPN-A 192.168.2.0
Local  Outgoing    Prefix            Bytes tag  Outgoing   Next Hop
tag    tag or VC   or Tunnel Id      switched   interface
None   18          192.168.2.0/24    0          Fa1/0      10.10.10.2


Verify (ISP-PE2):

sh ip bgp all
For address family: VPNv4 Unicast
BGP table version is 9, local router ID is 10.10.10.102
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
              r RIB-failure, S Stale
Origin codes: i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
Route Distinguisher: 1:100 (default for vrf VPN-A)
* >i172.16.1.0/30    10.10.10.101             0    100      0 ?
* > 172.16.2.0/30    0.0.0.0                  0         32768 ?
* >i192.168.1.0      10.10.10.101             0    100      0 ?
* > 192.168.2.0      172.16.2.2               0         32768 ?

sh mpls forwarding-table

Local  Outgoing    Prefix            Bytes tag  Outgoing   Next Hop
tag    tag or VC   or Tunnel Id      switched   interface
16     Pop tag     10.10.10.101/32   0          Fa1/0      10.10.10.1
17     Aggregate   172.16.2.0/30[V]  1456
18     Untagged    192.168.2.0/24[V] 570        Fa0/0      172.16.2.2

sh mpls forwarding-table vrf VPN-A 192.168.1.0
Local  Outgoing    Prefix            Bytes tag  Outgoing   Next Hop
tag    tag or VC   or Tunnel Id      switched   interface
None   18          192.168.1.0/24    0          Fa1/0      10.10.10.1

There we go. The traffic between the customer's private networks is now identified by the mpls label and hence could be forwarded to the proper VRF by the ISP-PE routers.

Verify reachability (Site1-Site2):

ping 192.168.2.1 source 192.168.1.1

Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.168.2.1, timeout is 2 seconds:
Packet sent with a source address of 192.168.1.1
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 32/52/100 ms



Теги: igp|eigrp|egp|leaking|ldp|vrf|mpls|vpn|label|bgp|route|cisco

DMVPN DUAL-HUB SINGLE-ROUTER

Topology:






HUB

SPOKE1

SPOKE2

IP routing table & EIGRP Topology at SPOKE1:

ISP1&ISP2 at HUB are operational:

IP-EIGRP Topology Table for AS(1)/ID(192.168.2.1)

Codes: P - Passive, A - Active, U - Update, Q - Query, R - Reply,
r - reply Status, s - sia Status

P 10.2.2.0/24, 1 successors, FD is 2562560
via Connected, Tunnel2
P 10.1.1.0/24, 1 successors, FD is 1281280
via Connected, Tunnel1
P 192.168.1.0/24, 1 successors, FD is 1409280
via 10.1.1.1 (1409280/128256), Tunnel1
via 10.2.2.1 (2690560/128256), Tunnel2
P 192.168.2.0/24, 1 successors, FD is 128256
via Connected, Loopback0
P 192.168.3.0/24, 1 successors, FD is 1410560
10.1.1.3 via 10.1.1.1 (1410560/1409280), Tunnel1
via 10.2.2.1 (2691840/1409280), Tunnel2



Gateway of last resort is 172.16.2.1 to network 0.0.0.0

172.16.0.0/24 is subnetted, 1 subnets
C 172.16.2.0 is directly connected, FastEthernet0/0
10.0.0.0/24 is subnetted, 2 subnets
C 10.2.2.0 is directly connected, Tunnel2
C 10.1.1.0 is directly connected, Tunnel1
D 192.168.1.0/24 [90/1409280] via 10.1.1.1, 00:09:11, Tunnel1
C 192.168.2.0/24 is directly connected, Loopback0
D 192.168.3.0/24 [90/1410560] via 10.1.1.3, 00:09:10, Tunnel1
S* 0.0.0.0/0 [1/0] via 172.16.2.1


ISP1 at HUB is DOWN:


IP-EIGRP Topology Table for AS(1)/ID(192.168.2.1)

Codes: P - Passive, A - Active, U - Update, Q - Query, R - Reply,
r - reply Status, s - sia Status

P 10.2.2.0/24, 1 successors, FD is 2562560
via Connected, Tunnel2
P 10.1.1.0/24, 1 successors, FD is 1281280
via Connected, Tunnel1
P 192.168.1.0/24, 1 successors, FD is 1409280
via 10.2.2.1 (2690560/128256), Tunnel2
P 192.168.2.0/24, 1 successors, FD is 128256
via Connected, Loopback0
P 192.168.3.0/24, 1 successors, FD is 2693120
10.2.2.3 via 10.2.2.1 (2693120/2690560), Tunnel2


Gateway of last resort is 172.16.2.1 to network 0.0.0.0

172.16.0.0/24 is subnetted, 1 subnets
C 172.16.2.0 is directly connected, FastEthernet0/0
10.0.0.0/24 is subnetted, 2 subnets
C 10.2.2.0 is directly connected, Tunnel2
C 10.1.1.0 is directly connected, Tunnel1
D 192.168.1.0/24 [90/2690560] via 10.2.2.1, 00:00:43, Tunnel2
C 192.168.2.0/24 is directly connected, Loopback0
D 192.168.3.0/24 [90/2693120] via 10.2.2.3, 00:00:43, Tunnel2
S* 0.0.0.0/0 [1/0] via 172.16.2.1



NEXT goal:

1) protect the mGRE tunnels with IPSEC
2) dual ISP at SPOKES

coming soon...

Теги: hub&spoke|full-mesh|dual-hub|vrf|dual-dmvpn|dmvpn|gre|gateway|ios|mgre|cisco

external ios-based media resources

Cisco IOS Router with or w/o DSP (hardware/software):

voice-card 0
 no dspfarm
 dsp services dspfarm

sccp local FastEthernet0/0
sccp ccm 1.1.1.2 identifier 2 priority 1 version 6.0
sccp ccm 1.1.1.1 identifier 1 priority 2 version 6.0
sccp
!
sccp ccm group 1
 bind interface FastEthernet0/0
 associate ccm 2 priority 1
 associate ccm 1 priority 2
 associate profile 1 register 2811-dsp
!
dspfarm profile 1 mtp
 codec g729r8
 maximum sessions software 50
 associate application SCCP

CUCM:

Media Resources - > MTP/Transcoder/Conference Bridge - > Add New
Device Name as stated in "associate profile 1 register" command.

Теги: conference bridge|mtp|transcoder|media termination point|pvdm|dspfarm|sccp|cisco|gateway|dsp|ios|voip

TCL B-ACD: Send alerting to ISP before connect

При использовании tcl-скрипта cisco не дает
станции Alert. Сразу идет Connect. Если скрипт не применять - все
нормально Alert затем Connect.

CSCdt67217
CSCdu49780
CSCdw72304

Both of these bugs did not have fixes integrated but rather worked around
either using a customized TCL script to provide alerting or use IOS 12.0(7)T.
Here is a snip of the modified script they used.....

proc act_Setup { } {

init_perCallVars
leg proceeding leg_incoming < --- send call proceeding
leg progress leg_incoming -p8 < --- send PI 8 after call proceeding
#leg alert leg_incoming -p 8 < --- may also use this construction
leg connect leg_incoming < --- connect call
# leg setupack leg_incoming < --- commented out so IOS ignores statement

отсюда. (ru.cisco.faq)

P.S.
Самого ringback всёравно можно и не услышать, т.к. ivr отвечает практически моментально после события connect. Можно лишь поиграться с delay проигрывания первого медиа-промта.

Теги: pri|tcl|e1|ios|gateway|cisco

CCME: outpulse DTMF after establishing call via E1 PRI

The problem is that in PRI digits are sent via q931 messages (see the specifications below), so a number with pauses (commas) will be represented as invalid. The workaround is to send the extra digits inband via DTMF after the call becomes connected.

ETSI ETS 300 102 "Integrated Services Digital Network (ISDN);
User-network interface layer 3 - Specifications for basic call control",
Section 4.5.8 - Called Party Number is 23 octets in length, with:
1st octet - (0xE0) IE Content
2nd octet - length of Called Party Number
3rd octet - Type/Plan
4th - 23rd octet - Called Number

Here is the solution:

'X' here is not a wildcard, just masked digits of real number.
',' (comma) indicates a pause.

voice translation-rule 103
    rule 1 /98926536ХХХХ/ /92101ХХ,416ХХХХ#8926536ХХХХ/

voice translation-profile to_suntel
    translate called 103

dial-peer voice 4 pots
  destination-pattern 92101ХХ
  no digit-strip
  port 0/3/0:15
  forward-digits extra inband


dial-peer voice 5 voip
  translation-profile incoming to_suntel
  incoming called-number 98926536XXXX


The other construction is possible, which is more compact and simple:

'....' here indicates the extension should be dialed after the call becomes connected.

dial-peer voice 6 pots
destination-patter 98926536XXXX....
forward-digits extra inband
prefix 8926536XXXX,,
port 0/3/0:15

Use with caution, this may cause an unexpected router reload, see cisco bug CSCsl60664 for more details.

Теги: dtmf|isdn|q931|pri|ccme|call manager|cisco|ios|e1|voip

Fax-to-Email combined onramp/offramp gateway template

!
hostname rtr-2811
!
ip domain name fax.local
!
voice class custom-cptone SI
 dualtone busy
  frequency 425
  cadence 505 480
 dualtone disconnect
  frequency 400
  cadence 500 500
!
fax receive called-subscriber $d$
fax send max-speed 9600
fax send transmitting-subscriber 5555555
fax interface-type fax-mail
mta send server 192.168.1.25 port 25
mta send subject FAX
mta send with-subject both
mta send postmaster postmaster@domain.ru
mta send mail-from hostname fax.local
mta send mail-from username rtr2821
mta receive aliases domain.local
mta receive aliases domain.ru
mta receive aliases [192.168.1.25]
mta receive maximum-recipients 5
mta receive generate mdn
!
application
 service offramp flash:app_faxmail_offramp.2.0.1.1.tcl
 !
 service onramp flash:app_faxmail_onramp.2.0.1.3.tcl
 !
 service fax_detect flash:app_fax_detect.2.1.2.3.tcl
 !
voice-port 0/0/0
 supervisory disconnect dualtone mid-call
 supervisory custom-cptone SI
 no battery-reversal
 disc_pi_off
 cptone RU
 timeouts call-disconnect 1
 timeouts wait-release 1
 connection plar opx 9999
 description -= CITY LINE =-
 caller-id enable
!

dial-peer voice 10 mmoip
 service fax_on_vfc_onramp_app out-bound
 destination-pattern 9999
 information-type fax
 session target mailto:user@domain.ru
!
dial-peer voice 555 pots
 service onramp
 incoming called-number 9999
 direct-inward-dial
 port 0/0/0
!
dial-peer voice 5 mmoip
 service offramp
 information-type fax
 incoming called-number 7777777
 dsn success
 dsn failure
!
dial-peer voice 1001 pots
 destination-pattern 7777777
 no digit-strip
 port 0/0/0
 forward-digits all
!

Теги: onramp offramp fax email fax2email faxto

One-Zone GateKeeper template

GK:

gatekeeper
 zone local gk.wg1 wg1 173.16.10.15
 no shutdown

GW:

interface Loopback0
 ip address 173.16.0.9 255.255.255.255
 h323-gateway voip interface
 h323-gateway voip id gk.wg1 ipaddr 173.16.10.15 1719
 h323-gateway voip h323-id GW9
 h323-gateway voip tech-prefix 2003
 h323-gateway voip bind srcaddr 173.16.0.9

Теги: gwgk|gatekeeper|ccme|ios|cisco|gateway|voip

Useful voice/voip debug comands

sh inventory
sh voice dsp detailed

sh voice port summary
sh voice port 0/2/0

sh dial-peer voice summary
sh dialplan number 2103

sh voice call status
sh voice call summary

sh call active voice brief
sh call active voice

sh ephone ..
sh telephony-service

sh gatekeeper endpoints
sh gatekeeper gw-type-prefix
sh gatekeeper calls
sh gatekeeper status

sh call resource voice stats
sh call resource voice threshold
sh gateway

csim start 2103

Теги: ccme|call manager|ios|debug

QOS DiffServ Template

!
access-list 150 permit tcp any any eq telnet
access-list 150 permit tcp any eq telnet any
!
class-map match-all TELNET
 match access-group 150
class-map match-all ICMP
 match protocol icmp
class-map match-any SIGNAL
 match protocol h323
 match protocol mgcp
 match protocol sip
 match protocol skinny
class-map match-all RTP-VIDEO
 match protocol rtp video
class-map match-all RTP
 match protocol rtp audio
!
!
policy-map OUT
 class RTP
    priority percent 70
  set dscp ef
 class RTP-VIDEO
    bandwidth remaining percent 70
  set ip dscp af41
 class SIGNAL
    bandwidth remaining percent 5
  set ip dscp af31
 class TELNET
    bandwidth remaining percent 5
  set ip dscp af21
 class ICMP
    bandwidth remaining percent 5
  set ip dscp af11
 class class-default
    bandwidth remaining percent 15
  set dscp default
!
interface Serial0/0/0
 service-policy output OUT   

Теги: ios|qos|cisco

call reject template

Call Blocking Specific Calling Numbers

Configure a voice translation rule to match the desired calling number you want to block.
This example uses 3927393.

    !
    voice translation-rule 1
     rule 1 reject /3927393/    
    !
    voice translation-profile call_block
     translate calling 1     
    !
    dial-peer voice … < pots|voip >                     < -- INCOMING!!! dial-peer
     call-block translation-profile incoming call_block    
     call-block disconnect-cause incoming call-reject
    !

Теги: ccme|call manager|cisco

SCCP for FXS + CUCM

sccp local FastEthernet0/1
sccp ccm 10.1.10.93 identifier 10 version 7,0
sccp ccm group 1
 associate ccm 10 priority 1
!
dial-peer voice … pots
 service stcapp
 default destination-pattern
!
voice-port …
 caller-id enable
!
sccp
stcapp
!


На ССМ:

Add SCCP gateway, MAC(last 10 chars) of sccp local interface
Add unit,subunit,config directory number for FXS/FXO ports.

Теги: sccp|call manager|cucm|cisco

MGCP template

ccm-manager mgcp
ccm-manager music-on-hold
ccm-manager config server 10.1.10.93 
ccm-manager config
!
mgcp
mgcp call-agent 10.1.10.93 2427 service-type mgcp version 0,1


dial-peer voice 1 pots
 service mgcpapp
 port 0/0/0
!


interface Serial0/1/0:15

 isdn bind-l3 ccm-manager
!


controller E1 0/1/0
 pri-group timeslots 1—14,16 service mgcp

!

Теги: mgcp|ccme|call manager|cisco

CUCM: own CDR analysis&reporting system

Лично меня жутко раздражает встроенная в CUCM система отчетности CAR. Вдаваться в детали особо не стану, для сомневающихся могу посоветовать сделать отчет top-talkers за месяц — результаты, прямо таки, плачевные. После довольно длительного анализа сети на предмет freeware-решений, ничего особо путного, кроме Asterisk-Stat (Больше подходит под логи ccmE, установка и настройка для интересующихся здесь), обнаружено не было.

Было принято решение написать что-нибудь простенькое, но своё, отвечающее следующим требованиям:

    *      Вывод истории звонков за определенный период (за час, текущий день, за вчера, за текущий месяц и т.п. с возможностью ручной установки границ выборки)
    *      Возможность вывести звонки, совершенные в рабочие/нерабочие часы
    *      Вывод top-talkers (главных «говорунов»), например, с длительностью разговора более 40 минут
    *      Вывод отдельно входящих/исходящих звонков, либо всех сразу
    *      Вывод списка звонков с определенного номера/на определенный номер
    *      Вывод списка звонков, сделанных определенным человеком (пофамильно).
На этом пункте делается основной упор, т.к. в CDR-ках хранится только userID звонящего, поэтому, если у Вас пользователи в CUCM импортированы из AD, к нему мы и будем обращаться для вывода ФИО/отдела звонящего. Можно, конечно, сделать выгрузку в базу один раз и постоянно брать данные из нее, но зато так Вы всегда можете ручаться за актуальность выводимых данных.

Итак, сначала нам нужен парсер, который будет обрабатывать CDR-файлы CUCM и записывать данные в базу (в данном случае, mysql). На самом CUCM делаем экспорт CDR куда-нибудь на FTP у себя в сети (Cisco Unified Serviceability — > Tools — > CDR Management), где впоследствии парсер их заберет и обработает.

Дабы не обременять себя лишним трудом, за основу был взят готовый парсер на python, предоставленный Aaron Paxson'ом здесь.

Ниже в комментариях опять же любезно нам советуют как адаптировать парсер под mysql и CUCM 6,0, а именно:

    * Добавляем в базу следующие столбцы:


origConversationId INTEGER

origMediaCap_Bandwidth INTEGER

destMediaCap_Bandwidth INTEGER

authorizationCodeValue VARCHAR(32)


    * Меняем везде PostgreSQL на MySQL
    * Добавляем commit (подтверждение записи в базу)
    * Добавляем код, удаляющий cMr-файлы (в рамках этой простой системки они нам не нужны)


Полный листинг скрипта-парсера:

#!/usr/bin/env python

import MySQLdb, sys, os
import string
import traceback
import logging
import zipfile
import glob

# Variables

#LOGGING=CRITICAL,ERROR,WARNING,INFO,DEBUG
loggingLevel=logging.ERROR

# Путь к логам парсера
logFilePath='/var/log/calldata-import.log'

# Путь к файлам СDR
cdr_path='/media/CDR'

# База MySQL
database='cdr'

# Сервер MySQL
dbhost='192.168.100.1'

# Логин/пасс на доступ к базе
dbuser='admin'
dbpassword='admin'

# Таблица в базе, куда будем инсертить данные
table='calldetails'

# Архив, куда будем складывать обработанные CDR-ы
fileArchive=cdr_path+'/archive.zip'

######## Functions ############
def createCdrSQL():
# TODO:  Refactor this so it's not CDR specific.  We'll need
# to also import CMR data as well
    totalRecordCount=0
    logging.debug("Creating CDR SQL")
    cdrListing = listCallFiles(cdr_path,"cdr")
    startSQL="INSERT INTO " + table + " ("
    fileCount=0

    logging.info("Parsing %d files" % len(cdrListing))
    for file in cdrListing:
        columns=""
        fileRecordCount=0
        try:
            logging.debug("Opening file %d of %d: %s" % (fileCount,len(cdrListing),file))
            cdrFile = open(cdr_path+"/"+file,'r')
            try:
                for line in cdrFile:

                    if line.startswith('"cdrRecordType"'):
                        logging.debug("Building columns...")
                        headers = line.split(',')
                        for header in headers:
                            newHeader=header.replace('"','')
                            columns+=newHeader+','
                        #There will be a trailing comma at the end.  Remove.
                        cleanedColumns=columns.rstrip(',')
                        cleanedColumns+=")"
                        logging.debug("Column data='" + cleanedColumns + "'")

                    elif not line.startswith('INTEGER'):
                        logging.debug("Building values...")
                        values=""
                        splitLineArray = line.split(',')
                        for value in splitLineArray:
                            if value == '""':
                                value = "null"
                            newValue=value.replace('"',"'")
                            values+=newValue+','

                        #Remove the trailing comma at the end
                        cleanedValues=values.rstrip(',')
                        cleanedValues+=");"
                        logging.debug("Value data='" + cleanedValues+"'")

                        logging.debug("Building full SQL statement")
                        fullSQL=startSQL+cleanedColumns+" VALUES (" + cleanedValues
                        logging.debug("Generated SQL is: " + fullSQL)

                        logging.debug("Inserting to DB")

                        try:
                            conn.query(fullSQL)
                            fileRecordCount+=1
                        except:
                            logging.critical("Unable to insert the following SQL: " + fullSQL)
                            logging.error(traceback.print_exc(file=sys.stdout))


            except:
                logging.warning("Could not enumerate files")
                cdrFile.close()
                logging.error(traceback.print_exc(file=sys.stdout))
        except:
            logging.critical("Could not open File")
        fileCount+=1
        totalRecordCount+=fileRecordCount
        logging.info("Inserted %d records for file %d of %d: %s" % (fileRecordCount,fileCount,len(cdrListing),file))
        logging.debug("closing file: %s" % (file))
        cdrFile.close()
        logging.info("Adding file to zip archive")
        addFileToZip(file)
        # Это подтверждение записи в базу. Без этого таблица остается пустая.
        conn.commit()
    conn.close()
    logging.info("Inserted %d records total" % (totalRecordCount))

def listCallFiles(dir,type):
    fileListing=[]
    files = os.listdir(dir)
    for file in files:
        if file.startswith(type):
            fileListing.append(file)
    return fileListing

def addFileToZip(file):
    logging.debug("Adding file %s to archive %s" % (file,fileArchive))

    #If the zip file exists, add to it, otherwise, create it.
    if os.path.exists(fileArchive):
        zippedFile=zipfile.ZipFile(fileArchive,'a')
    else:
        zippedFile=zipfile.ZipFile(fileArchive,'w')
  
    zippedFile.write(cdr_path+'/'+file,file)
    logging.debug("File: %s was successfully added to archive" % (file))
    zippedFile.close()
    os.remove(os.path.join(cdr_path, file))  
  
    #except:
    #    logging.error("Could not write to zip archive")
    #    zippedFile.close()

# Здесь мы ищем и удаляем все CMR-файлы в каталоге, они нам пока не нужны.
for each in glob.glob(cdr_path+'/'+'cmr*'):
    os.remove(each)

######### Enable Logging ###############
logging.basicConfig(level=loggingLevel,
                    format='%(asctime)s %(levelname)-8s %(message)s',
                    datefmt='%a, %d %b %Y %H:%M:%S',
                    filename=logFilePath,
                    filemode='a')

logging.info("Starting call-data import..")

########## Try to connect to the Database #############
try:
    conn = MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpassword,db=database)
except:
    # Ooops... couldn't connect
    logging.critical("Unable to connect to the database.  Terminating..")
    logging.error(traceback.print_exc(file=sys.stdout))
    sys.exit(2)

############ Insert SQL ####################################

createCdrSQL()


Запихиваем скрипт в crontab, убеждаемся, что база пополняется, CDR-ки удаляются и архивируются, и CMR-ки просто удаляются (пока).

Далее, делаем простую веб-форму (по желанию, с авторизацией) и, собственно, движок для поиска по базе.

Веб-форма (c авторизацией):

<  ?php

$valid_passwords = array ("admin" = > "12345");
$valid_users = array_keys($valid_passwords);

$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];

$validated = (in_array($user, $valid_users)) && ($pass == $valid_passwords[$user]);

if (!$validated) {
  header('WWW-Authenticate: Basic realm="Система отчетов по IP-телефонии"');
  header('HTTP/1.0 401 Unauthorized');
  die ("Неверное имя пользователя или пароль.");
}

// If arrives here, is a valid user.
echo "< center >Здравствуйте, < b >$user< /b >.< br >";
echo "Вы успешно авторизовались в системе.< /center >";

? >

< html >
  < head >

    < meta  http-equiv="Content-Type" content="text/html; charset=cp1251" >
    < title >Отчеты по телефонии< /title >
< link href=http://go.blog.ru/?";style.css" rel="stylesheet" type="text/css" >

    < !-- link calendar files  -- >
    < script language="JavaScript" src="t_c/calendar_eu.js" >< /script >
    < link rel="stylesheet" href=http://go.blog.ru/?";t_c/calendar.css" >
  < /head >
  < body  style=font-family:Verdana;font-size:10 >


< hr width=42% align=center >
< table class="table_up" bgcolor=#99CCFF align=center >
< tr >< td align=center COLSPAN=2 >
    < font size=4 >< b >Отчеты по телефонии< /b >< /font >
< /td >
< /tr >
< form  method="post" action="search.php?go" id="searchform" name="frm1" style="border:10" >
< tr >
< td  COLSPAN=2 >
Введите диапазон дат в формате YYYY-MM-DD (HH:MM:SS) или воспользуйтесь календарем:
< /td >< /tr >
< tr >< td align=center COLSPAN=2 >
< !--< SELECT NAME="vdate" > 
 < OPTION VALUE="1" selected > За сегодня< /option >
 < OPTION VALUE="2" > За вчера < /option >
 < OPTION VALUE="3" > За текущий месяц< /option >
 < OPTION VALUE="4" > За прошлый месяц< /option >
< /SELECT >
-- >

          < input  type="text" name="datefrom" value="" >
< script language="JavaScript" >
    var o_cal = new tcal ({
        // form name
        'formname': 'frm1',
        // input name
        'controlname': 'datefrom'
    });
    < /script >

< !-- Немного javascript. Tigra Calendar для удобной установки временных границ выборки.
      < input  type="text" name="dateto" value="" >
< script language="JavaScript" >
    var o_cal = new tcal ({
        // form name
        'formname': 'frm1',
        // input name
        'controlname': 'dateto'
    });       
    < /script >
< input type="checkbox" name="workhours" checked >< font size=1px >Рабочие часы

< input type="checkbox" name="offhours" >Нерабочие часы< /font >
< /td >
< /tr >
< tr >
< td align=center COLSPAN=2 >
    Длительность разговора больше, чем
    < input  type="text" name="duration" size=3 value="0" >&nbsp;минут

< /td >
< /tr >
< tr >
< td COLSPAN=2 >
< input type="radio" name="ctype" value="all" checked >Все звонки
< input type="radio" name="ctype" value="inc" > Входящие
< input type="radio" name="ctype" value="out" > Исходящие
< /td >
< /tr >
< tr >
< td COLSPAN=2 >
Звонили с номера:
< input  type="text" name="calling" value="" >&nbsp; на номер: < input  type="text" name="called" value="" >
< /td >
< /tr >
< tr >
< td >
Фамилия Звонившего:
< input type="text" name="fio" value="" >
< /td >
< td align=right width=13% >
< input type="submit" name="submit" value="Поиск" >
< /td >
< /tr >
< /table >
< /form >

< p align=center >
< a href=http://go.blog.ru/?";graph_b.php?y=2009" >График суммарной длительности исходящих звонков за пределы организации по месяцам< /a >
< br >
< a href=http://go.blog.ru/?";graph_p.php?y=2009" >График количества исходящих внешних звонков по месяцам< /a >
< /p >



< /body >
< /html >


 

Движок, берущий данные с веб-формы и строящий на их основании запрос к базе.

 

< html >
  < head >
    < meta  http-equiv="Content-Type" content="text/html" >
    < title >Отчеты по телефонии< /title >
< link href="http://go.blog.ru/?style.css" rel="stylesheet" type="text/css" >

  < /head >
< body >
< !-- Еще немного javascript. На этот раз ibox для наглядного вывода полной статистики по конкретному звонку из результатов поиска. -- >

< script type="text/javascript" src="ibox/ibox.js" >< /script >
< ?php
  if(isset($_POST['submit'])){
  if(isset($_GET['go'])){
//  if(preg_match("/^[  a-zA-Z]+/", $_POST['name'])){
//Забираем из полей формы данные
  $datefrom=$_POST['datefrom'];
  $dateto=$_POST['dateto'];
  $duration=$_POST['duration'];
 // $uid=$_POST['uid'];

if ($_POST['calling']){
  $calling=$_POST['calling'];
}else{
$calling="callingpartynumber";
}
if ($_POST['called']){
$called=$_POST['called'];
}else{
$called="finalcalledpartynumber";
}

//Забираем из полей формы данные
$fio=$_POST['fio'];
$sd=strtotime($datefrom);
$ed=strtotime($dateto);
  //Соединяемся с сервером БД
  $db=mysql_connect  ("localhost", "admin",  "cisco") or die ('I cannot connect to the database  because: ' . mysql_error());
  //-select  the database to use
  $mydb=mysql_select_db("cdr");

$ldap_host = "192.168.100.95";
   $ldap_port = "389";
// Корневой DN для поиска
   $base_dn = "ou=company,dc=company,dc=local";

   $atrib = array("cn","department");
   $atrib2 = array("cn","samaccountname");

// Задаем учетные данные для соединения с LDAP

   $ldap_user ="admin@company.local";
   $ldap_pass = "12345";
 
   $connect = ldap_connect( $ldap_host, $ldap_port);
   ldap_set_option($connect, LDAP_OPT_PROTOCOL_VERSION, 3);
 
    ldap_set_option($connect, LDAP_OPT_REFERRALS, 0);
   $bind = ldap_bind($connect, $ldap_user, $ldap_pass);


//Изменяем запрос, в зависимости от того, указано ли ФИО звонящего на форме или нет
if ($_POST['fio']) {
$fio = $_POST['fio'];
$filter2 = iconv ('CP1251','UTF-8',"(&(objectClass=user)(cn=$fio*))");
$read2 = ldap_search($connect, $base_dn, $filter2, $atrib2);
$info2 = ldap_get_entries($connect, $read2);
$uid = iconv('UTF-8', 'CP1251',$info2[0]["samaccountname"][0]);
$uid = "'" . $uid ."'";
} else {
$fio = "";
$uid = "callingpartyunicodeloginuserid";

}

//Забираем запрошенный тип звонков
$ctype = $_POST['ctype'];

//Определяем длину ANI/DNIS исходя из выбранного типа звонков на форме.
 if ($ctype == 'all') {
$len = " > 0";
}
else if ($ctype == 'inc') {
$len = " > 4";
}
else if ($ctype == 'out') {
$len = "< 5";
}

//Изменяем запрос, исходя из градации рабочие/нерабочие часы

if (isset($_POST['workhours']) && isset($_POST['offhours']) or !isset($_POST['workhours']) && !isset($_POST['offhours'])) {
$sql = "select datetimeorigination as 'DATE', callingpartyunicodeloginuserid as '
USER', callingpartynumber as 'FROM', finalcalledpartynumber as 'TO', duration, globalcallid_callid as 'CALL_ID'
 from calldetails where (FROM_UNIXTIME(datetimeorigination, '%Y-%m-%d') BETWEEN '$datefrom' and '$dateto' or FROM_UNIXTIME(datetimeorigination) BETWEEN '$datefrom' and '$dateto') and duration > $duration*60 and callingpartynumber = $calling and finalcalledpartynumber = $called and callingpartyunicodeloginuserid = $uid and LENGTH(callingpartynumber) $len";
}
elseif (isset($_POST['offhours'])) {
$sql = "select datetimeorigination as 'DATE', callingpartyunicodeloginuserid as '
USER', callingpartynumber as 'FROM', finalcalledpartynumber as 'TO', duration, globalcallid_callid as 'CALL_ID'
from calldetails where duration > $duration*60 and callingpartynumber = $calling and finalcalledpartynumber = $called and callingpartyunicodeloginuserid = $uid and LENGTH(callingpartynumber) $len and
FROM_UNIXTIME(datetimeorigination,'%Y-%m-%d') between '$datefrom' and '$dateto'
and (FROM_UNIXTIME(datetimeorigination,'%H:%i:%s') between '21:00:00' and '23:59:59' or FROM_UNIXTIME(datetimeorigination,'%H:%i:%s')
between '00:00:00' and '05:59:59')";
} elseif (isset($_POST['workhours'])) {
$sql = "select datetimeorigination as 'DATE', callingpartyunicodeloginuserid as '
USER', callingpartynumber as 'FROM', finalcalledpartynumber as 'TO', duration, globalcallid_callid as 'CALL_ID'
from calldetails where duration > $duration*60 and callingpartynumber = $calling and finalcalledpartynumber = $called and callingpartyunicodeloginuserid = $uid and LENGTH(callingpartynumber) $len and
FROM_UNIXTIME(datetimeorigination,'%Y-%m-%d') between '$datefrom' and '$dateto'
and (FROM_UNIXTIME(datetimeorigination,'%H:%i:%s') between '08:00:00' and '20:59:59')";
}
  //-Выполняем запрос к базе
  $result=mysql_query($sql);

$number = mysql_num_rows($result);

//Число строк = 0 - результатов нет

if ($number == 0) { echo "< p >< center >< h3 >По Вашему запросу записей в базе не найдено< /h3 >< /center >< /p >";} else {

//Для входящих звонков рисуем один вариант таблицы, для всех остальных - другой.

if ($ctype == 'inc') {
echo "< p align=center >< a href=http://go.blog.ru/?index.php >Вернуться на главную страницу< /a >< /p >";

//Делаем таблицу дял вывода
echo "< table class=table_up bgcolor=#99CCFF align=center >";
echo "< tr style=font-weight:bold; align=center >< td >ДАТА< /td >< td >ОТКУДА< /td >< td >КУДА< /td >< td >КОМУ< /td >< td >ОТДЕЛ< /td >< td >МИН< /td >< /tr >< /b >";
  while($row=mysql_fetch_array($result)){
          $date  =$row['DATE'];
          $from=$row['FROM'];
      $to=$row['TO'];   
// в CDR-ках длительность звонков хранится в секундах. Переводим в минуты, округляем до второго знака после запятой.
      $duration=round($row['duration']/60, 2);
      $id=$row['CALL_ID'];
// Важно задать кодировку при обращении к LDAP, иначе поиск не отработает.

       $filter = iconv ('CP1251','UTF-8',"(&(objectClass=user)(telephonenumber=$to))");
     
   $read = ldap_search($connect, $base_dn, $filter, $atrib);
  
   $info = ldap_get_entries($connect, $read);
 
// Выбираем только интересующие поля : cn и department.
   $dept = @iconv('UTF-8', 'CP1251',$info[0]["department"][0]);
   $cn = @iconv('UTF-8', 'CP1251',$info[0]["cn"][0]);


 echo "< tr >\n";

  echo "< td >< a rel=ibox title='Details for call id...' href=http://go.blog.ru/?seq.php?id=" . $id . " >" ." " . date('Y-m-d H:i:s', $date) . "< /a >< /td >< td > " . $from .  "&nbsp;< /td >< td > " . $to . "&nbsp;< /td >< td > " . $cn . "&nbsp;< /td >< td >" . $dept . "&nbsp;< /td >< td >" . $duration . "< /td >\n";

  echo "< /tr >";
  }
echo "< /table >";


} else {

echo "< p align=center >< a href=http://go.blog.ru/?index.php >Вернуться на главную страницу< /a >< /p >";

echo "< table class=table_up bgcolor=#99CCFF align=center >";
echo "< tr style=font-weight:bold; align=center >< b >< td >ДАТА< /td >< td >ОТКУДА< /td >< td >ПОЛЬЗОВАТЕЛЬ< /td >< td >ОТДЕЛ< /td >< td >КУДА< /td >< td >КОМУ< /td >< td >МИН< /td >< /tr >";
  while($row=mysql_fetch_array($result)){
          $date  =$row['DATE'];
          $user=$row['USER'];
          $from=$row['FROM'];
      $to=$row['TO'];   
      $duration=round($row['duration']/60, 2);
          $id=$row['CALL_ID'];
   
       $filter = iconv ('CP1251','UTF-8',"(&(objectClass=user)(samaccountname=$user))");
       $filter_num = iconv ('CP1251','UTF-8',"(&(objectClass=user)(telephoneNumber=$to))");
     
   $read = ldap_search($connect, $base_dn, $filter, $atrib);
   $read_num = ldap_search($connect, $base_dn, $filter_num, $atrib);
  
   $info = ldap_get_entries($connect, $read);
   $info_num = ldap_get_entries($connect, $read_num);
 
   $dept = @iconv('UTF-8', 'CP1251',$info[0]["department"][0]);
   $cn = @iconv('UTF-8', 'CP1251',$info[0]["cn"][0]);
   $cn_num = @iconv('UTF-8', 'CP1251',$info_num[0]["cn"][0]);

  echo "< tr >\n";
if ($date) {
  echo "< td >< a rel=ibox title='Details for call id...' href=http://go.blog.ru/?seq.php?id=" . $id . " >" . date('Y-m-d H:i:s', $date) . "< /td > < td > ". $from .  "&nbsp;< /td > < td > "   .$cn . "&nbsp;< /td >< td > " . $dept. "&nbsp;< /td >< td > " . $to . "&nbsp;< /td >< td > " . $cn_num . "&nbsp;  < td > " . $duration . "< /td >\n";
} else {
  echo "< td >" ." " . " " . "< /td >< td > "   .$cn . "&nbsp;< /td >< td > " . $dept. "&nbsp;< /td >< td > ". $from .  "&nbsp;< /td >< td > " . $to . "< /td >< td > " . $duration . "< /td >\n";
}
  echo "< /tr >";
  }
echo "< /table >";

}
echo "< p align=center >< a href=http://go.blog.ru/?index.php >Вернуться на главную страницу< /a >< /p >";

//Не забываем на всякий принудительно отключаться от LDAP и MYSQL

ldap_close($connect);

mysql_close($db);

  }
}
  else {
  echo  "< p >< center >< h2 >Неверный входной параметр< /h2 >< /center >< /p >";
  }

} else { echo "< p >< center >< h2 >Прямой доступ к этой странице запрещен< /h2 >< /center >< /p >";}




? >
< /body >
< /html >


 

update:

Пробуем сделать простенький график-гистограмму. Данные снова берем из той же БД.

На веб-форму добавляем простенькую ссылочку на скрипт:


< a href=http://go.blog.ru/?";graph.php?y=2009" >График суммарной длительности исходящих звонков за пределы организации< /a >


Далее нам снова потребуется движок для выборки данных из базы:





< ?php

 //Соединяемся с сервером БД
  $db=mysql_connect  ("localhost", "admin",  "admin") or die ('I cannot connect to the database because: ' . mysql_error());


  //Выбираем базу
  $mydb=mysql_select_db("cdr");
 
//Получаем параметр ГОД из адресной строки броузера. В дальнейшем можно будет сделать
//drop-down меню на веб-форме для удобства выбора интересующего года, пока нас
//интересует текущий год; Делаем 2 переменные, являющиеся диапазоном для выборки из БД.

$year = $_GET['y'];

$sd = "'$year-01-01'";

$ed = "'$year-12-31'";

//Составляем запрос.
//Снова определяем, что все исходящие вызовы за пределы компании имеют calledpartynumber >4.

$query = "select month(from_unixtime(datetimeorigination)) month, sum(duration) from calldetails" .
"where datetimeorigination BETWEEN unix_timestamp($sd) and unix_timestamp($ed) and" .
"LENGTH(finalcalledpartynumber) < > '4' group by month";

$res = mysql_query($query);

$i=0;

// Заполняем массив нулями, т.к. результат выборки естественно содержит только те месяца и
//суммы по duration, для которых в таблице присутствуют данные.
$fr = array_fill_keys(range(0,11),0);


//По циклу заполняем массив, в качестве порядкового номера для элемента используем
//полученный при выборке "месяц минус единица", первый элемент в массиве имеет порядковый номер "0".
while($row=mysql_fetch_array($res)){
$i=$row['month']-1;
$fr[$i]  =ceil($row['sum(duration)']/60);
$i++;
}


//Преобразуем массив в строку. Это нам нужно, чтобы передать данные через адресную строку скрипту, рисующему гистограмму.
$data = implode(",",$fr);


//Передаем данные, ссылаясь на image
echo "< img src=graph5.php?data=" . $data .  " >";

? >



Скрипт, рисующий график:


< ?php
// подключим библиотеки
include ("jpgraph_lib/jpgraph.php");
include ("jpgraph_lib/jpgraph_bar.php");

// определим массив данных (преобразовываем полученные из адресной строки элементы в обратно массив)

$datay=explode(",", $_GET['data']);


// делаем массив из 12 ти элементов-месяцев, нужен для подписей по оси Х.
$dy = Array ( 0 = > 'Jan', 1 = > 'Feb', 2 = > 'Mar', 3 = > 'Apr', 4 = > 'May', 5 = > 'Jun',
6 = > 'Jul', 7 = > 'Aug', 8 = > 'Sep', 9 = > 'Oct', 10 = > 'Nov', 11= > 'Dec' );


// создадим область для вывода диаграммы
$graph = new Graph(1000,800,"auto");

//Заголовок графика. По-русски, увы не понимает.
$graph- >title- >Set("Summary of Outgoing Call Durations by months, 2009");
$graph- >title- >SetFont(FF_FONT1,FS_BOLD);

//Форматирование легенды.
$graph- >legend- >SetFillColor('gray@0.8');
$graph- >legend- >SetLineWeight(2);
$graph- >legend- >SetShadow('gray@0.4',3);
$graph- >legend- >SetPos(0.01,0.04,'left','bottom');
$graph- >legend- >SetMarkAbsSize(1);
$graph- >legend- >SetFont(FF_FONT1,FS_BOLD);


// определим масштабирование по осям
$graph- >SetScale("textlin");

// добавим тень
$graph- >SetShadow(true, 3, array(222,222,222));

// определим отступ для области вывода
//$graph- >img- >SetMargin(50,30,20,40);
$graph- >img- >SetMargin(50,30,50,20);

// определим цвет отступа
$graph- >SetMarginColor('white');

// создадим рамку
$graph- >SetFrame(true,'gray',1);

// создадим диаграмму
$bplot = new BarPlot($datay);

// определим цвет заполнения столбцов
$bplot- >SetFillColor('#ff9900');

// покажем значения над каждым столбцом
$bplot- >value- >Show();

// установим формат вывода значений
$bplot- >value- >SetFormat('%d');

// установим цвет для значений
$bplot- >value- >SetColor('#0066ff');

// установим ширину столбцов
$bplot- >SetWidth(0.6);


//Легенда. Пока много не ясно, какой тип данных нужен для ее описания, пока что в сыром виде простой текст.
//Если кто знает как лучше добавить легенду для графика чирканите здесь или в ПМ на форуме.
$bplot- >SetLegend("Minutes");


// добавим диаграмму в область вывода
$graph- >Add($bplot);

// граница вокруг диаграммы
$graph- >SetBox(true, 'gray');

// покажем и определим сетки значений
$graph- >xgrid- >Show();
$graph- >xgrid- >SetLineStyle('dashed');
$graph- >xgrid- >SetColor('gray');
$graph- >ygrid- >SetLineStyle('dashed');
$graph- >ygrid- >SetColor('gray');
$graph- >ygrid- >SetFill(true,'#EFEFEF@0.5','#CCCCCC@0.5');

// спрячем метки на осях
$graph- >xaxis- >HideTicks();
$graph- >yaxis- >HideTicks();


$graph- >xaxis- >SetTickLabels($dy);


// установим цвет осей и подписей
$graph- >xaxis- >SetColor('darkgray', 'darkgray');
$graph- >yaxis- >SetColor('darkgray', 'darkgray');

// определим шрифт для вывода подписей на осях
$graph- >xaxis- >title- >SetFont(FF_VERDANA,FS_NORMAL);
$graph- >yaxis- >title- >SetFont(FF_VERDANA,FS_NORMAL);

// определим отступ сверху
$graph- >yaxis- >scale- >SetGrace(10);

// отобразим результат
$graph- >Stroke();
? >


Выглядеть график будет примерно так:



Собственно, это всё. Поднималась эта системка на обычной убунте, apache2,2/mysql/php5/python все последнее из синаптика.

Что планируется добавить (TO-DO):

    * Исключительно для наглядности, более детально проработать вопрос графиков (библиотека GD для php).
    * Оптимизация кода, в частности структуры запросов к базе.

Теги: cucm|call manager|cdr|cisco|python|php

RSS - подписка