Заказ IP-детализации из личного кабинета
Материал из BiTel WiKi
(Различия между версиями)
(Новая страница: «= Описание = Сразу оговоримся, что всё описанное реализовывалось в версии 5.2. На данный мом…») |
|||
Строка 25: | Строка 25: | ||
</ol> | </ol> | ||
- | = | + | = Установка = |
- | + | === jar === | |
+ | |||
+ | 4 класса, запакованные в jar-архив dsi_inet_ip_detail.jar: | ||
<ul> | <ul> | ||
<li><b>ru.dsi.bgbilling.modules.inet.accounting.detail.event.InetClientDetailCreateEvent</b> - событие заказа детализации, передаваемое через ActiveMQ из BGBillingServer в BGInetAccounting. Расширяет стандартный InetDetailCreateEvent.</li> | <li><b>ru.dsi.bgbilling.modules.inet.accounting.detail.event.InetClientDetailCreateEvent</b> - событие заказа детализации, передаваемое через ActiveMQ из BGBillingServer в BGInetAccounting. Расширяет стандартный InetDetailCreateEvent.</li> | ||
Строка 33: | Строка 35: | ||
<li><b>ru.dsi.bgbilling.modules.inet.accounting.detail.bean.ClientDetailUtils</b> - управление таблицей, в которой храним информацию о заказах детализации из Web-статистики по cid договора</li> | <li><b>ru.dsi.bgbilling.modules.inet.accounting.detail.bean.ClientDetailUtils</b> - управление таблицей, в которой храним информацию о заказах детализации из Web-статистики по cid договора</li> | ||
</ul> | </ul> | ||
- | |||
- | |||
Архив кладём в /usr/bgbilling/lib/app/ и обновляем аккаунтинг-сервера через ./update.sh, чтобы классы были доступны как серверу, так и аккаунтингам. | Архив кладём в /usr/bgbilling/lib/app/ и обновляем аккаунтинг-сервера через ./update.sh, чтобы классы были доступны как серверу, так и аккаунтингам. | ||
+ | |||
+ | === Таблицы === | ||
+ | |||
+ | <source lang=sql> | ||
+ | CREATE TABLE `custom_inet_client_detail_task` ( | ||
+ | `cid` int(11) NOT NULL, | ||
+ | `dt` datetime NOT NULL, | ||
+ | PRIMARY KEY (`cid`) | ||
+ | ) | ||
+ | </source> | ||
+ | |||
+ | === Динамический код === | ||
+ | Веб-экшен: | ||
+ | <source lang="java"> | ||
+ | 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()); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </source> | ||
+ | |||
+ | Класс для сериализации информации о файле: | ||
+ | <source lang=java> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | === XSL === | ||
+ | |||
+ | === Конфигурация === | ||
+ | |||
+ | inet-accounting.xml: | ||
+ | |||
+ | <source lang="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> | ||
+ | |||
+ | </source> |
Версия 01:20, 27 мая 2014
Содержание |
Описание
Сразу оговоримся, что всё описанное реализовывалось в версии 5.2.
На данный момент (26.05.2014) в стандартном ЛК для модуля Inet нет возможности заказа IP-детализации по flow, в отличие от модулей Dialup и IPN. Ниже приводится решение этой проблемы самостоятельно.
Последовательность действий для внедрения:
- Пишем собственный обработчик для BGInetAccounting, обрабатывающий задания на создание файлов детализации из ActiveMQ
- Настраиваем обработчик, рестартуем аккаунтинги
- Пишем Web-action в динамическом коде для ЛК
- Настраиваем экшен, рестартуем сервер, чтобы подгрузить классы вне динамического кода
- Рисуем веб-интерфейс для экшена
Порядок действий при заказе детализации:
- Клиент в ЛК выбирает день, месяц и год и жмёт "заказать детализацию"
- Выдаётся сообщение "Детализация будет доступна для скачивания через несколько минут"
- Пока задание для этого cid не выполнится, либо не истечёт таймаут, клиенту выдаётся это сообщение и в новом запросе будет отказано
- Аккаунтинг создаёт файл в определённой в конфиге директории, в субдиректории договора (по cid)
- Файл появляется у клиента в ЛК и доступен для скачивания через веб-экшен по http
- Клиент может удалить любой свой созданный файл детализации из ЛК
- Клиент не может хранить больше X файлов детализации, для создания следующего необходимо удалить что-то. X задаётся в параметрах экшена,по-умолчанию 10
Установка
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-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>