* 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.
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
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
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
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
При использовании 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 проигрывания первого медиа-промта.
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.
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
! 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
Лично меня жутко раздражает встроенная в 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-файлы (в рамках этой простой системки они нам не нужны)
# Путь к логам парсера 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)
########## 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)
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 >
< input type="text" name="datefrom" value="" > < script language="JavaScript" > var o_cal = new tcal ({ // form name 'formname': 'frm1', // input name 'controlname': 'datefrom' }); < /script >
< !-- Немного javascript. для удобной установки временных границ выборки. < 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 >Рабочие часы
< 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. На этот раз для наглядного вывода полной статистики по конкретному звонку из результатов поиска. -- >
< 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");
//Изменяем запрос, в зависимости от того, указано ли ФИО звонящего на форме или нет 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, иначе поиск не отработает.
} 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);
//Легенда. Пока много не ясно, какой тип данных нужен для ее описания, пока что в сыром виде простой текст. //Если кто знает как лучше добавить легенду для графика чирканите здесь или в ПМ на форуме. $bplot- >SetLegend("Minutes");
// добавим диаграмму в область вывода $graph- >Add($bplot);
// граница вокруг диаграммы $graph- >SetBox(true, 'gray');
// установим цвет осей и подписей $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). * Оптимизация кода, в частности структуры запросов к базе.