Заказ IP-детализации из личного кабинета

Материал из BiTel WiKi

(Различия между версиями)
Перейти к: навигация, поиск
(Описание)
(XSL)
Строка 598: Строка 598:
<xsl:call-template name="dayFilterForm">
<xsl:call-template name="dayFilterForm">
<xsl:with-param name="withSubmit" select="0"/>
<xsl:with-param name="withSubmit" select="0"/>
-
<xsl:with-param name="withZeroDay" select="0"/>
 
</xsl:call-template>
</xsl:call-template>
</div>
</div>

Версия 01:41, 27 мая 2014

Содержание

Описание

Файл:inet-ip-detail.png

Сразу оговоримся, что всё описанное реализовывалось в версии 5.2.

На данный момент (26.05.2014) в стандартном ЛК для модуля Inet нет возможности заказа IP-детализации по flow, в отличие от модулей Dialup и IPN. Ниже приводится решение этой проблемы самостоятельно.

Последовательность действий для внедрения:

  1. Пишем собственный обработчик для BGInetAccounting, обрабатывающий задания на создание файлов детализации из ActiveMQ
  2. Настраиваем обработчик, рестартуем аккаунтинги
  3. Пишем Web-action в динамическом коде для ЛК
  4. Настраиваем экшен, рестартуем сервер, чтобы подгрузить классы вне динамического кода
  5. Рисуем веб-интерфейс для экшена

Порядок действий при заказе детализации:

  1. Клиент в ЛК выбирает день, месяц и год и жмёт "заказать детализацию"
  2. Выдаётся сообщение "Детализация будет доступна для скачивания через несколько минут"
  3. Пока задание для этого cid не выполнится, либо не истечёт таймаут, клиенту выдаётся это сообщение и в новом запросе будет отказано
  4. Аккаунтинг создаёт файл в определённой в конфиге директории, в субдиректории договора (по cid)
  5. Файл появляется у клиента в ЛК и доступен для скачивания через веб-экшен по http
  6. Клиент может удалить любой свой созданный файл детализации из ЛК
  7. Клиент не может хранить больше X файлов детализации, для создания следующего необходимо удалить что-то. X задаётся в параметрах экшена,по-умолчанию 10

Установка

jar

Файл:Dsi inet ip detail.zip

(Нужно переименовать .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&amp;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&amp;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"/>&amp;mid=<xsl:value-of select="/data/@mid"/>&amp;</xsl:variable>
	<table class="filter" style="margin-bottom: 5pt;">
                <tr>
			<td style="padding-right:5pt;"><a href="#" onclick="document.location='{$URI}action=TrafficReport&amp;operation=execute'; return false;">Обычные трафики</a></td>
               	        <td style="padding-right:5pt;"><a href="#" onclick="document.location='{$URI}action=TrafficMaxReport&amp;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}&amp;action=IpDetail&amp;operation=download&amp;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}&amp;action=IpDetail&amp;operation=download&amp;filename={@name}">
						<img src="img/save.png" alt="Скачать"/></a>
					</td>
					<td><a href="{$URI}&amp;action=IpDetail&amp;operation=remove&amp;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
Личные инструменты