17 changed files with 451 additions and 86 deletions
-
2README.md
-
0abonapp/templatetags/__init__.py
-
10abonapp/templatetags/dpagination.py
-
56agent/core.py
-
18agent/mod_mikrotik.py
-
1agent/settings.py.example
-
6bugs.txt
-
3chatbot/telebot.py
-
10devapp/dev_types.py
-
2djing/settings_example.py
-
2docs/bot.md
-
286docs/dev.md
-
62docs/install.md
-
23docs/netflow.md
-
2requirements.txt
-
3setup.py
-
51templates/toolbar_page.html
@ -0,0 +1,10 @@ |
|||||
|
from django import template |
||||
|
|
||||
|
register = template.Library() |
||||
|
|
||||
|
|
||||
|
@register.simple_tag |
||||
|
def url_page_replace(request, field, value): |
||||
|
dict_ = request.GET.copy() |
||||
|
dict_[field] = value |
||||
|
return dict_.urlencode() |
||||
@ -1,14 +1,8 @@ |
|||||
- (GUI) Иконки возле кнопок не настроены, натыканы случайно |
- (GUI) Иконки возле кнопок не настроены, натыканы случайно |
||||
- В abonapp.complete_service нельзя досрочно завершить услугу пока нет связи с NAS'ом |
- В abonapp.complete_service нельзя досрочно завершить услугу пока нет связи с NAS'ом |
||||
- Не меняет приоритеты заказанных услуг (UNIQUE constraint failed) |
|
||||
- Надо указывать в /usr/lib/python3.5/site-packages/django/contrib/auth/decorators.py raise_exception=True |
- Надо указывать в /usr/lib/python3.5/site-packages/django/contrib/auth/decorators.py raise_exception=True |
||||
- В Mikrotik надо редиректить тех, у кого нет доступа в сеть |
|
||||
- Пароли абонентов надо шифровать ключом для паролей |
- Пароли абонентов надо шифровать ключом для паролей |
||||
- Доделать везде переводы |
- Доделать везде переводы |
||||
- Не надо коннектиться к микротику когда не собираемся ничего изменять. А то при сохранении залогинились и вышли без действий |
- Не надо коннектиться к микротику когда не собираемся ничего изменять. А то при сохранении залогинились и вышли без действий |
||||
- Не удаляет просроченные услуги если не пингуется NAS |
- Не удаляет просроченные услуги если не пингуется NAS |
||||
- Надо отменить учёт временной зоны |
|
||||
!!! Обязательно проверить как отрабатывает на NAS удаление и изменение AbonTariff |
|
||||
!!! Удалить всё что связано с активацией услуги |
|
||||
!!! Убрать досрочное завершение услуги |
|
||||
! Проверить дату завершения услуги |
! Проверить дату завершения услуги |
||||
@ -0,0 +1,2 @@ |
|||||
|
# Оповещения из биллнга |
||||
|
Мгновенная связь с администратором и общение с сотрудниками. |
||||
@ -0,0 +1,286 @@ |
|||||
|
>Перед началом обязательно, хотя бы поверхностно, ознакомиться с документацией к |
||||
|
[Django](https://docs.djangoproject.com). |
||||
|
|
||||
|
## Добавление поддерживаемого устройства (Свича) |
||||
|
Для того чтоб добавить новый тип устройства с которым потом сможет работать биллинг нужно открыть файл *devapp/dev_types.py* |
||||
|
и переопределить 2 интерфейса. Первый это *BasePort* для порта свича, а второй *DevBase* для самого свича соответственно. |
||||
|
|
||||
|
Разберём этот процесс на примере готовой реализации для Eltex. |
||||
|
|
||||
|
```python |
||||
|
class EltexPort(BasePort): |
||||
|
|
||||
|
def __init__(self, snmpWorker, *args, **kwargs): |
||||
|
BasePort.__init__(self, *args, **kwargs) |
||||
|
assert issubclass(snmpWorker.__class__, SNMPBaseWorker) |
||||
|
self.snmp_worker = snmpWorker |
||||
|
|
||||
|
# выключаем этот порт |
||||
|
def disable(self): |
||||
|
self.snmp_worker.set_int_value( |
||||
|
"%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num), |
||||
|
2 |
||||
|
) |
||||
|
|
||||
|
# включаем этот порт |
||||
|
def enable(self): |
||||
|
self.snmp_worker.set_int_value( |
||||
|
"%s.%d" % ('.1.3.6.1.2.1.2.2.1.7', self.num), |
||||
|
1 |
||||
|
) |
||||
|
``` |
||||
|
Тут в инициилизации мы передаём все базовые параметры базовому конструктору, и дополнительный аргумент snmpWorker |
||||
|
для работы по SNMP. *snmpWorker* это объект реализованного интерфейса SNMPBaseWorker, далее я опишу где мы его реализуем. |
||||
|
Для порта надо переопределить 2 метода: *disable* и *enable* понятно для чего, чтоб включать и отключать порт. |
||||
|
|
||||
|
Шаблон реализации можно даже не менять, просто укажите вместо строки .1.3.6.1.2.1.2.2.1.7 нужный SNMP OID для включения порта. |
||||
|
К этой строке будет добавляться номер порта который нужно включить. |
||||
|
Для отключения так-же по аналогии. |
||||
|
|
||||
|
Теперь реализация для свича: |
||||
|
```python |
||||
|
class EltexSwitch(DLinkDevice): |
||||
|
|
||||
|
@staticmethod |
||||
|
def description(): |
||||
|
return _('Eltex switch') |
||||
|
|
||||
|
def get_ports(self): |
||||
|
#nams = self.get_list('.1.3.6.1.4.1.171.10.134.2.1.1.100.2.1.3') |
||||
|
stats = self.get_list('.1.3.6.1.2.1.2.2.1.7') |
||||
|
oper_stats = self.get_list('.1.3.6.1.2.1.2.2.1.8') |
||||
|
#macs = self.get_list('.1.3.6.1.2.1.2.2.1.6') |
||||
|
speeds = self.get_list('.1.3.6.1.2.1.31.1.1.1.15') |
||||
|
res = [] |
||||
|
for n in range(28): |
||||
|
res.append(EltexPort(self, |
||||
|
n+1, |
||||
|
'',#nams[n] if len(nams) > 0 else _('does not fetch the name'), |
||||
|
True if int(stats[n]) == 1 else False, |
||||
|
'',#macs[n] if len(macs) > 0 else _('does not fetch the mac'), |
||||
|
int(speeds[n]) if len(speeds) > 0 and int(oper_stats[n]) == 1 else 0, |
||||
|
)) |
||||
|
return res |
||||
|
|
||||
|
def get_device_name(self): |
||||
|
return self.get_item('.1.3.6.1.2.1.1.5.0') |
||||
|
|
||||
|
def uptime(self): |
||||
|
uptimestamp = safe_int(self.get_item('.1.3.6.1.2.1.1.3.0')) |
||||
|
tm = RuTimedelta(timedelta(seconds=uptimestamp/100)) or RuTimedelta(timedelta()) |
||||
|
return tm |
||||
|
|
||||
|
@staticmethod |
||||
|
def has_attachable_to_subscriber(): |
||||
|
return False |
||||
|
|
||||
|
@staticmethod |
||||
|
def is_use_device_port(): |
||||
|
return False |
||||
|
``` |
||||
|
Метод **@description** Просто отображает человекопонятное название вашего устройства в биллинге. |
||||
|
Заметьте что строка на английском и заключена в процедуру **_** (это ugettext_lazy, см. в импорте вверху файла), |
||||
|
это локализация для текущего языка. Про локализацию можно почитать в соответствующем разделе [django translation](https://docs.djangoproject.com/en/1.9/topics/i18n/translation/). |
||||
|
|
||||
|
Метод **@get_ports** чаще всего редко изменяется по алгоритму, так что вам, в большенстве случаев, достаточно добавить |
||||
|
нужные SNMP OID в соответствующие места процедуры. Но вы вольны реализовать ваш метод получения портов |
||||
|
как вам угодно, главное чтоб возвращался список объектов определённого выше класса порта для этого свича. |
||||
|
В данном случае возвращается список объектов *EltexPort*. |
||||
|
|
||||
|
Метод **@get_device_name** получает по SNMP имя устройства, просто укажите в вашей реализации нужный OID. |
||||
|
|
||||
|
Метод **@uptime**, понятно что возвращает, укажите нужный OID. Вернётся тип *RuTimedelta*, это не тип Django, я сам его реализовал |
||||
|
для локализации временного промежутка на русский. |
||||
|
|
||||
|
Статический метод **@has_attachable_to_subscriber** возвращает правду если это устройство можно привязать к абоненту. |
||||
|
Например у Dlink стоит True потому что Dlink стоит во многих местах на доступе, и его порты принадлежат |
||||
|
абонентам при авторизации. |
||||
|
|
||||
|
Статический метод **@is_use_device_port** используется в DHCP чтоб понять что мы используем для привязки к абоненту всё устройство или |
||||
|
только порт устройства. Например, если у устройства только 1 порт абонента (PON ONU), то нужно вернуть True, во всех остальных случаях False. |
||||
|
|
||||
|
Реализация SNMPBaseWorker по сути не нужна, класс абстрактных методов не имеет. |
||||
|
Потому когда наследуем наследуемся от *DevBase* то в базовые классы добавим и SNMPBaseWorker, как это сделано в *DLinkDevice*: |
||||
|
```python |
||||
|
class DLinkDevice(DevBase, SNMPBaseWorker): |
||||
|
|
||||
|
def __init__(self, ip, snmp_community, ver=2): |
||||
|
DevBase.__init__(self) |
||||
|
SNMPBaseWorker.__init__(self, ip, snmp_community, ver) |
||||
|
``` |
||||
|
А далее просто передадим параметры для конструкторов обоих базовых классов. |
||||
|
|
||||
|
Вы, наверное, обратили внимание, что *EltexSwitch* наследован от *DLinkDevice*, это потому что некоторые методы идентичны, |
||||
|
и реализация для обоих свичей похожа. |
||||
|
|
||||
|
>П.С. Не изучайте как пример реализацию для PON, она, как по мне, костыльна. Это связано с тем что PON сильно отличается от |
||||
|
>принципа работы обычного свича, и чтоб подружить свичи и PON был реализован такой костыль. |
||||
|
|
||||
|
|
||||
|
## Добавим платёжную систему |
||||
|
Для того чтоб добавить платёжную систему добавьте в файл *abonapp/pay_systems.py* процедуру которая будет принимать |
||||
|
request, далее он пригодится в теле вашей процедуры. это тот самый request который передаётся в *view*. Пустая процедура, возвращающая xml, будет выглядеть так: |
||||
|
|
||||
|
```python |
||||
|
def my_custom_pay_system(request): |
||||
|
return "<?xml version='1.0' encoding='UTF-8'?>\n" \ |
||||
|
"<pay-response>Pay ok</pay-response>\n" |
||||
|
``` |
||||
|
|
||||
|
Затем импортируйте её в процедуру *terminal_pay* в файле views.py каталога abonapp. |
||||
|
Для примера это будет выглядеть так: |
||||
|
|
||||
|
```python |
||||
|
@atomic |
||||
|
def terminal_pay(request): |
||||
|
from .pay_systems import my_custom_pay_system |
||||
|
ret_text = my_custom_pay_system(request) |
||||
|
return HttpResponse(ret_text) |
||||
|
``` |
||||
|
|
||||
|
Проследите чтоб ваша процедура не вызывала исключений, обрабатывайте всё внутри тела процедуры. |
||||
|
Про декоратор **@atomic** вы можете прочитать в документации к [Django](https://docs.djangoproject.com/en/1.9/topics/db/transactions). |
||||
|
В кратце этот декоратор защищает от незавешённых транзакций, например при высокой нагрузке. |
||||
|
|
||||
|
|
||||
|
## Реализация своего NAS |
||||
|
Сейчас биллинг работает с Mikrotik в роли устройства для доступа абонентов в интернет. |
||||
|
Как можно реализовать такой-же для вашего роутера, например на GNU/Linux. |
||||
|
|
||||
|
Создадим файл *agent/mod_linux.py* и реализуем потомка для интерфейса *BaseTransmitter*. |
||||
|
Методы вашего класса будут вызываться биллингом для взаимодействия с сервером доступа абонентов в интернет(NAS). |
||||
|
|
||||
|
```python |
||||
|
from .core import BaseTransmitter, NasFailedResult, NasNetworkError |
||||
|
|
||||
|
class LinuxTransmitter(BaseTransmitter): |
||||
|
|
||||
|
def add_user_range(self, user_list): |
||||
|
"""добавляем список абонентов в NAS""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(AbonStruct) |
||||
|
def remove_user_range(self, users): |
||||
|
"""удаляем список абонентов""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(AbonStruct) |
||||
|
def add_user(self, user, *args): |
||||
|
"""добавляем абонента""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(AbonStruct) |
||||
|
def remove_user(self, user): |
||||
|
"""удаляем абонента""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(AbonStruct) |
||||
|
def update_user(self, user, *args): |
||||
|
""" |
||||
|
Чтоб обновить абонента можно изменить всё кроме его uid, по uid абонент будет найден. |
||||
|
Это значит что вы можете передать объект user класса AbonStruct, где только uid будет указывать на абонента, |
||||
|
а остальные поля будут содержать новое значение. |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(TariffStruct) |
||||
|
def add_tariff_range(self, tariff_list): |
||||
|
""" |
||||
|
Пока не используется, зарезервировано. |
||||
|
Добавляет список тарифов в NAS |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(TariffStruct) |
||||
|
def remove_tariff_range(self, tariff_list): |
||||
|
""" |
||||
|
Пока не используется, зарезервировано. |
||||
|
Удаляем список тарифов по уникальным идентификаторам |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(TariffStruct) |
||||
|
def add_tariff(self, tariff): |
||||
|
""" |
||||
|
Пока не используется, зарезервировано. |
||||
|
Добавляем тариф |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(TariffStruct) |
||||
|
def update_tariff(self, tariff): |
||||
|
""" |
||||
|
Пока не используется, зарезервировано. |
||||
|
Чтоб обновить тариф надо изменить всё кроме его tid, по tid тариф будет найден |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(TariffStruct) |
||||
|
def remove_tariff(self, tid): |
||||
|
""" |
||||
|
:param tid: id тарифа в среде NAS сервера чтоб удалить по этому номеру |
||||
|
Пока не используется, зарезервировано. |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
@check_input_type(TariffStruct) |
||||
|
def ping(self, host, count=10): |
||||
|
""" |
||||
|
:param host: ip адрес в текстовом виде, например '192.168.0.1' |
||||
|
:param count: количество пингов |
||||
|
:return: None если не пингуется, иначе кортеж, в котором (сколько вернулось, сколько было отправлено) |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
def read_users(self): |
||||
|
""" |
||||
|
Читаем пользователей с NAS |
||||
|
:return: список AbonStruct |
||||
|
""" |
||||
|
``` |
||||
|
|
||||
|
Для того чтоб биллинг знал о вашем классе надо указать его в *agent/\_\_init\_\_.py*. |
||||
|
Замените |
||||
|
>from .mod_mikrotik import MikrotikTransmitter |
||||
|
|
||||
|
На это |
||||
|
>from .mod_mikrotik import LinuxTransmitter |
||||
|
|
||||
|
И укажите ваш класс |
||||
|
> Transmitter = MikrotikTransmitter |
||||
|
|
||||
|
Получится примерно такое содержимое: |
||||
|
|
||||
|
```python |
||||
|
from .mod_mikrotik import LinuxTransmitter |
||||
|
from .core import NasFailedResult, NasNetworkError |
||||
|
from .structs import TariffStruct, AbonStruct |
||||
|
|
||||
|
Transmitter = LinuxTransmitter |
||||
|
``` |
||||
|
|
||||
|
Для примера, как вы наверное уже догадались, можно посмотреть реализацию для Mikrotik в файле *agent/mod_mikrotik.py* |
||||
|
|
||||
|
Чтобы выводить в биллинге различные сообщения об ошибках есть 2 типа исключений: *NasFailedResult* и *NasNetworkError*. |
||||
|
NasNetworkError, как понятно из названия, вызывается при проблемах в сети. А NasFailedResult при ошибочных кодах возврата из модуля на сервере NAS. |
||||
|
|
||||
|
Биллинг прослушивает эти исключения при выполнении, и при возбуждении этих исключений отображает текст ошибки на экране пользователя. |
||||
|
|
||||
|
При переопределении базового класса пожалуйста не забывайте вызвать базовый метод чтоб отработали декораторы методов интерфейса, этот декоратор проверяет тип входных данных. |
||||
|
Динамическая типизация python иногда подкладывает свинью в том смысле что можно передать не то что вы хотели бы передать, потому типы лучше проконтролировать, и тогда интерпретатор станет вашим другом помошником :) |
||||
|
|
||||
|
Когда я прошу вызвать базовый метод, я имею ввиду это: |
||||
|
```python |
||||
|
... |
||||
|
def add_user_range(self, user_list): |
||||
|
super(LinuxTransmitter, self).add_user_range(user_list) |
||||
|
# ваш код |
||||
|
... |
||||
|
``` |
||||
|
|
||||
|
Кстати, не все методы обязательно реализовывать, некоторые из них зарезервированы на будущие цели, в комментариях к их прототипам в интерфейсе *BaseTransmitter* это сказано. |
||||
|
Поэтому просто переопределите эти зарезервированные методы как пустые, например метод *add_tariff_range* нигде в биллинге пока не вызывается. так что можно определить его пустым. |
||||
|
```python |
||||
|
def add_tariff_range(self, tariff_list): |
||||
|
pass |
||||
|
``` |
||||
@ -0,0 +1,23 @@ |
|||||
|
### Сбор информации трафика по netflow |
||||
|
|
||||
|
Установим flow-tools |
||||
|
|
||||
|
Fedora: |
||||
|
|
||||
|
> dnf install -y flow-tools flow-tools-devel |
||||
|
|
||||
|
Затем надо собрать утилиту для преобразования flow в запрос для mysql. |
||||
|
Возьмём её из github: |
||||
|
``` |
||||
|
cd /var/www/djing/agent/netflow/ |
||||
|
git clone https://github.com/nerosketch/djing_flow.git djing_flow_git |
||||
|
cd djing_flow_git/ |
||||
|
make |
||||
|
mv djing_flow ../ |
||||
|
cd .. |
||||
|
rm -rf djing_flow_git |
||||
|
``` |
||||
|
|
||||
|
Инструкцию по использованию можно найти на странице [djing_flow](https://github.com/nerosketch/djing_flow). |
||||
|
Посмотреть пример работы можно так: |
||||
|
|
||||
@ -0,0 +1,3 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
import os |
||||
|
import sys |
||||
@ -1,29 +1,24 @@ |
|||||
{% with request.GET.urlencode|yesno:'&,' as start_divide %} |
|
||||
{% with request.GET.urlencode as url %} |
|
||||
{% if pag.paginator.num_pages > 1 %} |
|
||||
<div class="row"> |
|
||||
<div class="col-sm-4 col-sm-offset-4"> |
|
||||
<ul class="pagination"> |
|
||||
{% if pag.number == 1 %} |
|
||||
<li class="disabled"><a href="#">«</a></li> |
|
||||
{% else %} |
|
||||
<li><a href="?{{ url }}{{ start_divide }}p=1">«</a></li> |
|
||||
{% endif %} |
|
||||
{% if pag.has_previous %} |
|
||||
<li><a href="?{{ url }}{{ start_divide }}p={{ pag.previous_page_number }}">{{ pag.previous_page_number }}</a></li> |
|
||||
{% endif %} |
|
||||
<li class="disabled"><a href="#">{{ pag.number }}</a></li> |
|
||||
{% if pag.has_next %} |
|
||||
<li><a href="?{{ url }}{{ start_divide }}p={{ pag.next_page_number }}">{{ pag.next_page_number }}</a></li> |
|
||||
{% endif %} |
|
||||
{% if pag.number == pag.paginator.num_pages %} |
|
||||
<li class="disabled"><a href="#">»</a></li> |
|
||||
{% else %} |
|
||||
<li><a href="?{{ url }}{{ start_divide }}p={{ pag.paginator.num_pages }}">»</a></li> |
|
||||
{% endif %} |
|
||||
</ul> |
|
||||
</div> |
|
||||
|
{% load dpagination %} |
||||
|
<div class="row"> |
||||
|
<div class="col-sm-4 col-sm-offset-4"> |
||||
|
<ul class="pagination"> |
||||
|
{% if pag.number == 1 %} |
||||
|
<li class="disabled"><a href="#">«</a></li> |
||||
|
{% else %} |
||||
|
<li><a href="?{% url_page_replace request 'p' 1 %}">«</a></li> |
||||
|
{% endif %} |
||||
|
{% if pag.has_previous %} |
||||
|
<li><a href="?{% url_page_replace request 'p' pag.previous_page_number %}">{{ pag.previous_page_number }}</a></li> |
||||
|
{% endif %} |
||||
|
<li class="disabled"><a href="#">{{ pag.number }}</a></li> |
||||
|
{% if pag.has_next %} |
||||
|
<li><a href="?{% url_page_replace request 'p' pag.next_page_number %}">{{ pag.next_page_number }}</a></li> |
||||
|
{% endif %} |
||||
|
{% if pag.number == pag.paginator.num_pages %} |
||||
|
<li class="disabled"><a href="#">»</a></li> |
||||
|
{% else %} |
||||
|
<li><a href="?{% url_page_replace request 'p' pag.paginator.num_pages %}">»</a></li> |
||||
|
{% endif %} |
||||
|
</ul> |
||||
</div> |
</div> |
||||
{% endif %} |
|
||||
{% endwith %} |
|
||||
{% endwith %} |
|
||||
|
</div> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue