Заказ IP-детализации из личного кабинета
Материал из BiTel WiKi
Внимание! Данное решение/метод/статья относится к версии 5.2 и для других версий может быть неактуальна! Вам нужно самостоятельно поправить решение под свои нужды или воспользоваться помощью на форуме. Будем признательны, если внизу страницы или отдельной статьёй вы разместите исправленное решение для другой версии или подсказки что надо исправить.
Содержание |
Описание
Сразу оговоримся, что всё описанное реализовывалось в версии 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: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
--Cromeshnic 01:44, 27 мая 2014 (UTC)