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

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

Перейти к: навигация, поиск

Содержание

Описание

Сразу оговоримся, что всё описанное реализовывалось в версии 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

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>
Личные инструменты