Заказ IP-детализации из личного кабинета
Материал из BiTel WiKi
(→XSL) |
(→Конфигурация) |
||
Строка 668: | Строка 668: | ||
</application> | </application> | ||
+ | </source> | ||
+ | |||
+ | Конфигурация сервера (сервис->настройка->конфигурация): | ||
+ | <source lang=ini> | ||
+ | dynaction:inet.web.ActionIpDetail=ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail | ||
+ | custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail.30.path=/usr/local/ipdetail | ||
+ | #таймаут в секундах, после которого задание на создание детализации считается "потерянным" | ||
+ | custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail.30.timeout=300 | ||
</source> | </source> |
Версия 01:35, 27 мая 2014
Содержание |
Описание
Сразу оговоримся, что всё описанное реализовывалось в версии 5.2.
На данный момент (26.05.2014) в стандартном ЛК для модуля Inet нет возможности заказа IP-детализации по flow, в отличие от модулей Dialup и IPN. Ниже приводится решение этой проблемы самостоятельно.
Последовательность действий для внедрения:
- Пишем собственный обработчик для BGInetAccounting, обрабатывающий задания на создание файлов детализации из ActiveMQ
- Настраиваем обработчик, рестартуем аккаунтинги
- Пишем Web-action в динамическом коде для ЛК
- Настраиваем экшен, рестартуем сервер, чтобы подгрузить классы вне динамического кода
- Рисуем веб-интерфейс для экшена
Порядок действий при заказе детализации:
- Клиент в ЛК выбирает день, месяц и год и жмёт "заказать детализацию"
- Выдаётся сообщение "Детализация будет доступна для скачивания через несколько минут"
- Пока задание для этого cid не выполнится, либо не истечёт таймаут, клиенту выдаётся это сообщение и в новом запросе будет отказано
- Аккаунтинг создаёт файл в определённой в конфиге директории, в субдиректории договора (по cid)
- Файл появляется у клиента в ЛК и доступен для скачивания через веб-экшен по http
- Клиент может удалить любой свой созданный файл детализации из ЛК
- Клиент не может хранить больше X файлов детализации, для создания следующего необходимо удалить что-то. X задаётся в параметрах экшена,по-умолчанию 10
Установка
jar
(Нужно переименовать .zip в .jar)
4 класса, запакованные в jar-архив dsi_inet_ip_detail.jar:
- ru.dsi.bgbilling.modules.inet.accounting.detail.event.InetClientDetailCreateEvent - событие заказа детализации, передаваемое через ActiveMQ из BGBillingServer в BGInetAccounting. Расширяет стандартный InetDetailCreateEvent.
- ru.dsi.bgbilling.modules.inet.accounting.detail.InetClientDetailWorker - воркер в контексте BGInetAccounting, отлавливающий события заказа детализации
- ru.dsi.bgbilling.modules.inet.accounting.detail.InetFlowClientDetailMaker - рабочий класс для собственно создания файлов детализации. Расширяет стандартный InetFlowDetailMaker, переопределяя метод сохранения файлов.
- ru.dsi.bgbilling.modules.inet.accounting.detail.bean.ClientDetailUtils - управление таблицей, в которой храним информацию о заказах детализации из Web-статистики по cid договора
Архив кладём в /usr/bgbilling/lib/app/ и обновляем аккаунтинг-сервера через ./update.sh, чтобы классы были доступны как серверу, так и аккаунтингам.
Таблицы
CREATE TABLE `custom_inet_client_detail_task` ( `cid` int(11) NOT NULL, `dt` datetime NOT NULL, PRIMARY KEY (`cid`) )
Динамический код
Веб-экшен:
package ru.dsi.bgbilling.modules.inet.api.server.action.web; import bitel.billing.common.TimeUtils; import ru.bitel.bgbilling.common.BGException; import ru.bitel.bgbilling.kernel.container.web.action.AbstractAction; import ru.bitel.bgbilling.kernel.event.EventProcessor; import ru.bitel.bgbilling.modules.inet.accounting.detail.event.InetDetailCreateEvent; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServ; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetServType; import ru.bitel.bgbilling.modules.inet.api.common.bean.InetSessionLog; import ru.bitel.bgbilling.modules.inet.api.common.service.InetServService; import ru.bitel.bgbilling.modules.inet.api.server.bean.InetDeviceMap; import ru.bitel.bgbilling.modules.inet.api.server.bean.InetSessionDao; import ru.bitel.bgbilling.modules.inet.api.server.bean.InetSessionLogDao; import ru.bitel.bgbilling.server.util.ServerUtils; import ru.bitel.common.RangeUtils; import ru.bitel.common.inet.IpAddress; import ru.bitel.common.io.IOUtils; import ru.bitel.common.model.Period; import ru.bitel.oss.systems.inventory.resource.server.bean.DeviceInterfaceIndexDao; import ru.dsi.bgbilling.modules.inet.accounting.detail.bean.ClientDetailUtils; import ru.dsi.bgbilling.modules.inet.accounting.detail.event.InetClientDetailCreateEvent; import ru.dsi.bgbilling.modules.inet.api.common.bean.DetailFileInfo; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import javax.xml.ws.Holder; import java.io.*; import java.sql.SQLException; import java.util.*; /** * @author cromeshnic@gmail.com * * Параметры глобального конфига: * - путь к директории с файлами клиентской детализации (со структурой субдиректорий вида cid/filename.zip) * custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail.[mid].path * - таймаут, после которого задание считается подвисшим и клиент может заказать новую детализацию * custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail.[mid].timeout=300 */ public class ActionIpDetail extends AbstractAction { @Resource InetServService servService; private Period period = null; public void execute() throws BGException { List<InetServ> inetServList = this.servService.inetServList(this.cid); setResultParam("servs", inetServList); //Нужно только для того, чтобы запилить /data/day, month, year в xml if(this.period==null){ this.period = getPeriodByYearMonthDay(); } checkState(); buildList(); } /** * Обработка запроса детализации * Параметры: * serv_id * year * month * day * @throws BGException */ public void request() throws BGException { int currentFileCount = this.buildList(); //Берём только date1 из периода int inetServId = this.req.getInt("serv_id", -1); InetServ serv = this.servService.inetServGet(inetServId); if(serv==null){ execute(); return; } //Проверяем, есть ли уже выполняемое задание на генерацию логов и не устарело ли оно? try { Date taskDt = ClientDetailUtils.getTaskDt(this.cid, this.con); if(taskDt != null ){ if(!this.expired(taskDt)){ execute(); return; } } } catch (SQLException e) { throw new BGException(e); } //Максимальное разрешенное количество файлов int maxCount = this.setup.getInt("custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail."+this.mid+".fileCountLimit", 10); if(currentFileCount>=maxCount){ this.setResultParam("maxCountError", String.valueOf(maxCount)); execute(); return; } this.period = getPeriodByYearMonthDay(); Date day = period.getDateFrom(); Date timeFrom = TimeUtils.clear_HOUR_MIN_MIL_SEC(day); Calendar calendarTo = Calendar.getInstance(); calendarTo.setTime(day); TimeUtils.clear_HOUR_MIN_MIL_SEC(calendarTo); calendarTo.add(Calendar.DAY_OF_MONTH, 1); calendarTo.add(Calendar.SECOND, -1); Date timeTo = TimeUtils.convertCalendarToDate(calendarTo); List<InetSessionLog> sessionLogList = new InetSessionLogDao(this.con, this.mid, day).list( null, Collections.singleton(this.cid), null, Collections.singleton(serv.getId()), null, null, timeFrom, timeTo, true, null); sessionLogList.addAll( new InetSessionDao(this.con, this.mid).listAsLog( this.context.getDirectory(InetServType.class, this.mid, true).list(), null, null, Collections.singleton(this.cid), Collections.singleton(serv.getId()), timeFrom, timeTo, null, 0, 0, true) ); List<InetDetailCreateEvent.SessionInfo> sessionInfoList = sessionInfoList(serv, sessionLogList); try { ClientDetailUtils.registerTask(this.cid, this.con); } catch (SQLException e) { throw new BGException(e); } ServerUtils.commitConnection(this.con); EventProcessor.getInstance().publish( new InetClientDetailCreateEvent( this.cid, this.mid, serv.getDeviceId(), serv.getId(), serv.getTitle(), timeFrom, timeTo, sessionInfoList, null)); execute(); } public void remove() throws BGException{ String fileName = this.req.get("filename", null); String path = getPath(); if(fileName==null || path == null){ execute(); return; } if(!fileName.matches("^[A-Za-z0-9_]+\\.zip$")){ execute(); return; } final File file = new File(path+File.separator+String.valueOf(this.cid), fileName); if(!file.exists()){ execute(); return; } file.delete(); execute(); } public void download() throws BGException{ final String fileName = this.req.get("filename", null); String path = getPath(); if(fileName==null || path == null){ execute(); return; } if(!fileName.matches("^[A-Za-z0-9_]+\\.zip$")){ execute(); return; } final File file = new File(path+File.separator+String.valueOf(this.cid), fileName); if(!file.exists()){ execute(); return; } HttpServletResponse resp = this.context.getResponse(); resp.setContentType("application/zip"); resp.setHeader("Content-Disposition", "attachment; filename=" + fileName); Holder<DataHandler> data = new Holder<DataHandler>(); data.value = new DataHandler(new DataSource() { public String getContentType() { return "text/plain"; } public InputStream getInputStream() throws IOException { return new FileInputStream(file); } public String getName() { return fileName; } public OutputStream getOutputStream() throws IOException { return null; } }); try{ IOUtils.transfer((data.value).getInputStream(), resp.getOutputStream(), 10240); (data.value).getInputStream().close(); } catch (Exception ex) { throw new BGException(ex); } } private List<InetDetailCreateEvent.SessionInfo> sessionInfoList(InetServ inetServ, List<InetSessionLog> sessionLogList) throws BGException { List<InetDetailCreateEvent.SessionInfo> sessionInfoList = new ArrayList<InetDetailCreateEvent.SessionInfo>(); InetDeviceMap inetDeviceMap = InetDeviceMap.getInstance(this.mid); Map<Integer,Map<Integer,List<DeviceInterfaceIndexDao.DeviceInterfaceIndexItem>>> deviceifaceIndexMap = new HashMap<Integer,Map<Integer,List<DeviceInterfaceIndexDao.DeviceInterfaceIndexItem>>>(); DeviceInterfaceIndexDao interfaceIndexDao = new DeviceInterfaceIndexDao(this.con, this.mid); long[] intersection = new long[2]; for (InetSessionLog sessionLog : sessionLogList) { byte[] addressFrom = sessionLog.getInetAddressBytes(); byte[] addressTo = addressFrom; if ((addressFrom == null) || (addressFrom.length == 0)) { addressFrom = inetServ.getAddressFrom(); addressTo = inetServ.getAddressTo(); } if ((addressFrom == null) || (addressFrom.length == 0)) { InetServType servType = this.servService.inetServTypeGet(inetServ.getTypeId()); if (servType.isAddressAllInterface()) { addressFrom = null; addressTo = null; } else { //print("Address range not found for detail (inetServ:" + inetServ.getId() + ", session:" + sessionLog.getId() + "). Skipping"); continue; } } InetDeviceMap.InetDeviceMapItem device = inetDeviceMap.get(sessionLog.getDeviceId()); for (Map.Entry<Integer, List<Integer>> e : device.getFlowAgentIfaceMap().entrySet()) { Integer agentDeviceId = e.getKey(); Set<Integer> protoIfaces = new HashSet<Integer>(e.getValue()); if ((agentDeviceId == inetServ.getDeviceId()) && (inetServ.getInterfaceId() >= 0) && ((protoIfaces.size() != 1) || (!protoIfaces.contains(Integer.valueOf(-1))))) { protoIfaces = new HashSet<Integer>(Collections.singleton(inetServ.getInterfaceId())); } Date timeFrom = sessionLog.getSessionStart(); Date timeTo = sessionLog.getSessionStop(); if (timeTo == null) { timeTo = new Date(); } Set<Integer> ifaces = new HashSet<Integer>(); Map<Integer, List<DeviceInterfaceIndexDao.DeviceInterfaceIndexItem>> ifaceIndexMap = DeviceInterfaceIndexDao.getIfaceIndexMap(interfaceIndexDao, deviceifaceIndexMap, agentDeviceId); for (Integer iface : protoIfaces) { List<DeviceInterfaceIndexDao.DeviceInterfaceIndexItem> ifaceIndexList = ifaceIndexMap.get(iface); if ((ifaceIndexList == null) || (ifaceIndexList.size() == 0)) { ifaces.add(iface); } else { for (DeviceInterfaceIndexDao.DeviceInterfaceIndexItem index : ifaceIndexList) { if (RangeUtils.intersectionAnd(intersection, index.millisFrom, index.millisTo, timeFrom.getTime(), timeFrom.getTime()) != null) { ifaces.add(index.index); break; } } ifaces.add(iface); } } sessionInfoList.add(new InetDetailCreateEvent.SessionInfo(timeFrom, timeTo, agentDeviceId, ifaces, new IpAddress(addressFrom), new IpAddress(addressTo), null)); } } return sessionInfoList; } private String getPath(){ return this.setup.get("custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail."+this.mid+".path", null); } /** * Рисуем список файлов * @return количество файлов в списке */ private int buildList(){ int result = 0; String path = this.getPath(); if(path==null){ return result; } path = path + File.separator+ String.valueOf(this.cid); File dir = new File(path); if(!dir.exists()){ return result; } List<DetailFileInfo> fileList = new ArrayList<DetailFileInfo>(); File[] files = dir.listFiles(); if(files==null){ return 0; } DetailFileInfo fileInfo; for (File file : files) { if (!file.isDirectory() && file.getName().endsWith(".zip")) { fileInfo = new DetailFileInfo(); fileInfo.setName(file.getName()); fileInfo.setLastModified(new Date(file.lastModified())); fileInfo.setSize(file.length()); fileList.add(fileInfo); } } //Упорядочиваем по дате заказа : последние заказанные будут первыми в списке Collections.sort(fileList,new Comparator<DetailFileInfo>() { @Override public int compare(DetailFileInfo o1, DetailFileInfo o2) { return (o1.getLastModified().getTime()>o2.getLastModified().getTime()? -1 : 1); } }); setResult(fileList); result = fileList.size(); return result; } /** * Выдаём последнюю дату заказа детализации, если есть * @throws BGException */ private void checkState() throws BGException{ try { Date taskDt = ClientDetailUtils.getTaskDt(this.cid, this.con); if(taskDt == null ){ return; } //Если дата ещё не устарела, то всё ок if(!expired(taskDt)){ setResultParam("processing", "1");//Помечаем, что ждём окончания выполнения текущего задания } } catch (SQLException e) { throw new BGException(e); } } private boolean expired(Date taskDt){ int timeout = this.setup.getInt("custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail." + this.mid + ".timeout", 300); Calendar expires = Calendar.getInstance(); expires.setTime(taskDt); expires.add(Calendar.SECOND, timeout); return expires.before(Calendar.getInstance()); } }
Класс для сериализации информации о файле:
package ru.dsi.bgbilling.modules.inet.api.common.bean; import ru.bitel.common.xml.JAXBUtils; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.util.Date; /** * Информация о файле детализации в ЛК */ public class DetailFileInfo { protected String name; protected Date lastModified; protected long size;//filesize in bytes @XmlAttribute public String getName() { return name; } public void setName(String name) { this.name = name; } @XmlAttribute @XmlJavaTypeAdapter(JAXBUtils.DateTimeAdapter.class) public Date getLastModified() { return lastModified; } public void setLastModified(Date lastModified) { this.lastModified = lastModified; } @XmlAttribute public long getSize() { return size; } public void setSize(long size) { this.size = size; } }
XSL
В inet.xsl указываем:
<xsl:import href="tpl.IpDetail.xsl"/>
А также прописываем в inet.xsl ссылки на наш экшен по вкусу - меняем в 2 местах
<table class="filter" style="margin-bottom: 5pt;"> <tr> <td style="padding-right:5pt;"><b>Обычные трафики</b></td> <td><a href="#" onclick="document.location='{$URI}action=TrafficMaxReport&operation=execute'; return false;">Превалирующие трафики</a></td> </tr> </table>
на:
<table class="filter" style="margin-bottom: 5pt;"> <tr> <td style="padding-right:5pt;"><b>Обычные трафики</b></td> <td style="padding-right:5pt;"><a href="#" onclick="document.location='{$URI}action=TrafficMaxReport&operation=execute'; return false;">Превалирующие трафики</a></td> <td><a href="#" onclick="document.location='{$URI}action=IpDetail'; return false;">IP-детализация</a></td> </tr> </table>
И самое главное - создаём файл tpl.IpDetail.xsl:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan = "http://xml.apache.org/xalan" xmlns:java="http://xml.apache.org/xalan/java" xmlns:sql="bitel.billing.server.util.exslt.SQLQuery" xmlns:common="http://common.bitel.ru" > <xsl:template name="IpDetail"> <xsl:variable name="URI"><xsl:value-of select="$WEBEXECUTER"/>?module=<xsl:value-of select="/data/@module"/>&mid=<xsl:value-of select="/data/@mid"/>&</xsl:variable> <table class="filter" style="margin-bottom: 5pt;"> <tr> <td style="padding-right:5pt;"><a href="#" onclick="document.location='{$URI}action=TrafficReport&operation=execute'; return false;">Обычные трафики</a></td> <td style="padding-right:5pt;"><a href="#" onclick="document.location='{$URI}action=TrafficMaxReport&operation=execute'; return false;">Превалирующие трафики</a></td> <td><b>IP-детализация</b></td> </tr> </table> <xsl:if test="/data/maxCountError"> <xsl:call-template name="error"> <xsl:with-param name="text">Достигнуто максимальное количество сохранённых файлов детализации (<xsl:value-of select="/data/maxCountError"/>). Удалите старые файлы для заказа новых.</xsl:with-param> </xsl:call-template> </xsl:if> <xsl:if test="/data/processing"> <div class="infoMessage">Детализация будет доступна для скачивания через несколько минут <br/> <a href="{$URI}action=IpDetail">Обновить</a> </div> </xsl:if> <xsl:if test="not(/data/processing)"> <form method='post' action='{$WEBEXECUTER}'> <input type="hidden" name="operation" value="request"/> <xsl:call-template name="action"/> <xsl:call-template name="error"/> <table class="filter"> <tr> <td colspan="2"> <table><tr> <td>Сервис:</td> <td style="width:100%"> <select name="serv_id" style="width:100%"> <xsl:call-template name="servList"/> </select> </td> </tr></table> </td> </tr> <tr> <td style="text-align:right; vertical-align:top;"> <div align="right"> <xsl:call-template name="dayFilterForm"> <xsl:with-param name="withSubmit" select="0"/> <xsl:with-param name="withZeroDay" select="0"/> </xsl:call-template> </div> </td> <td style="padding-left: 5px; text-align:right; vertical-align:top;"> <xsl:call-template name="submit"> <xsl:with-param name="title" select="'Заказать детализацию'"/> </xsl:call-template> </td> </tr> </table> </form> </xsl:if> <!-- file list --> <table class="table800"> <thead> <tr> <td>Файл</td> <td>Размер (MB)</td> <td>Изменён</td> <td>Скачать</td> <td>Удалить</td> </tr> </thead> <tbody> <xsl:for-each select="common:result/data/item"> <tr> <td> <a href="{$URI}&action=IpDetail&operation=download&filename={@name}"> <xsl:value-of select="@name"/> </a> </td> <td><xsl:value-of select="format-number(@size div 1048576.0 , '#0.00' )"/></td> <td><xsl:value-of select="@lastModified"/></td> <td><a href="{$URI}&action=IpDetail&operation=download&filename={@name}"> <img src="img/save.png" alt="Скачать"/></a> </td> <td><a href="{$URI}&action=IpDetail&operation=remove&filename={@name}"> <img src="img/remove.png" alt="Удалить"/></a> </td> </tr> </xsl:for-each> </tbody> </table> </xsl:template> </xsl:stylesheet>
Конфигурация
inet-accounting.xml:
<application context="accounting"> ... <param name="detail.client.dir" value="/usr/local/ipdetail" /> ... <context name="collector"> <context name="detail"> <!-- Cоздание обработчика flow детализации --> <bean name="detailWorker" class="ru.bitel.bgbilling.modules.inet.accounting.detail.InetDetailWorker"/> <!-- Собственный обработчик flow детализации из ЛК--> <bean name="clientDetailWorker" class="ru.dsi.bgbilling.modules.inet.accounting.detail.InetClientDetailWorker"/> </context> </context> </application>
Конфигурация сервера (сервис->настройка->конфигурация):
dynaction:inet.web.ActionIpDetail=ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail.30.path=/usr/local/ipdetail #таймаут в секундах, после которого задание на создание детализации считается "потерянным" custom.ru.dsi.bgbilling.modules.inet.api.server.action.web.ActionIpDetail.30.timeout=300