Browse Source

Merge branch 'devel' of https://github.com/nerosketch/djing into devel

devel
Dmitry Novikov 9 years ago
parent
commit
920bb15e1c
  1. 2
      README.md
  2. 0
      abonapp/templatetags/__init__.py
  3. 10
      abonapp/templatetags/dpagination.py
  4. 56
      agent/core.py
  5. 18
      agent/mod_mikrotik.py
  6. 1
      agent/settings.py.example
  7. 6
      bugs.txt
  8. 3
      chatbot/telebot.py
  9. 10
      devapp/dev_types.py
  10. 2
      djing/settings_example.py
  11. 2
      docs/bot.md
  12. 286
      docs/dev.md
  13. 62
      docs/install.md
  14. 23
      docs/netflow.md
  15. 2
      requirements.txt
  16. 3
      setup.py
  17. 51
      templates/toolbar_page.html

2
README.md

@ -5,3 +5,5 @@
## Содержание ## Содержание
* [Установка](./docs/install.md) * [Установка](./docs/install.md)
* [Разработка расширений](./docs/dev.md)
* [Сбор информации трафика по netflow](./docs/netflow.md)

0
djing/utils/save_from_nodeny.py → abonapp/templatetags/__init__.py

10
abonapp/templatetags/dpagination.py

@ -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()

56
agent/core.py

@ -13,18 +13,18 @@ class NasNetworkError(Exception):
pass pass
# Проверяет входной тип на принадлежность классу.
# Можно передать объект или коллекцию объектов
# Проверяет входной объект на принадлежность типу.
# Можно передать несколько типов в соответствии количеству параметров
# В общем желание организовать строгую типизацию :) # В общем желание организовать строгую типизацию :)
def check_input_type(class_or_type):
def check_input_type(*types):
def real_check(fn): def real_check(fn):
def wrapped(self, user):
try:
for usr in user:
assert isinstance(usr, class_or_type)
except TypeError:
assert isinstance(user, class_or_type)
return fn(self, user)
def wrapped(self, *args):
if len(types) != len(args):
raise AttributeError("length of @types must be equivalent for length of @args")
for param_type, param in zip(types, args):
if not isinstance(param, param_type):
raise TypeError("%s must be %s, but is %s" % (str(param), str(param_type), type(param)))
return fn(self, *args)
return wrapped return wrapped
return real_check return real_check
@ -56,40 +56,45 @@ class BaseTransmitter(metaclass=ABCMeta):
def update_user(self, user, *args): def update_user(self, user, *args):
"""чтоб обновить абонента можно изменить всё кроме его uid, по uid абонент будет найден""" """чтоб обновить абонента можно изменить всё кроме его uid, по uid абонент будет найден"""
@abstractmethod
@check_input_type(AbonStruct)
def pause_user(self, user):
"""Приостановить обслуживание абонента"""
@abstractmethod
@check_input_type(AbonStruct)
def start_user(self, user):
"""Продолжить обслуживание абонента"""
@abstractmethod @abstractmethod
@check_input_type(TariffStruct) @check_input_type(TariffStruct)
def add_tariff_range(self, tariff_list): def add_tariff_range(self, tariff_list):
"""добавляем список тарифов в NAS"""
"""
Пока не используется, зарезервировано.
Добавляет список тарифов в NAS
"""
@abstractmethod @abstractmethod
@check_input_type(TariffStruct) @check_input_type(TariffStruct)
def remove_tariff_range(self, tariff_list): def remove_tariff_range(self, tariff_list):
"""удаляем список тарифов по уникальным идентификаторам"""
"""
Пока не используется, зарезервировано.
Удаляем список тарифов по уникальным идентификаторам
"""
@abstractmethod @abstractmethod
@check_input_type(TariffStruct) @check_input_type(TariffStruct)
def add_tariff(self, tariff): def add_tariff(self, tariff):
"""добавляем тариф"""
"""
Пока не используется, зарезервировано.
Добавляет тариф
"""
@abstractmethod @abstractmethod
@check_input_type(TariffStruct) @check_input_type(TariffStruct)
def update_tariff(self, tariff): def update_tariff(self, tariff):
"""чтоб обновить тариф надо изменить всё кроме его tid, по tid тариф будет найден"""
"""
Пока не используется, зарезервировано.
Чтоб обновить тариф надо изменить всё кроме его tid, по tid тариф будет найден
"""
@abstractmethod @abstractmethod
@check_input_type(TariffStruct) @check_input_type(TariffStruct)
def remove_tariff(self, tid): def remove_tariff(self, tid):
"""удаляем тариф"""
"""
:param tid: id тарифа в среде NAS сервера чтоб удалить по этому номеру
Пока не используется, зарезервировано.
"""
@abstractmethod @abstractmethod
@check_input_type(TariffStruct) @check_input_type(TariffStruct)
@ -129,4 +134,3 @@ class BaseTransmitter(metaclass=ABCMeta):
print(la) print(la)
self.remove_user_range( list_for_del ) self.remove_user_range( list_for_del )
self.add_user_range( list_for_add ) self.add_user_range( list_for_add )

18
agent/mod_mikrotik.py

@ -162,7 +162,7 @@ class TransmitterManager(BaseTransmitter, metaclass=ABCMeta):
def _exec_cmd(self, cmd): def _exec_cmd(self, cmd):
assert isinstance(cmd, list) assert isinstance(cmd, list)
result_iter = self.ar.talk_iter(cmd)
result_iter = '<not request>'#self.ar.talk_iter(cmd)
res = [] res = []
for rt in result_iter: for rt in result_iter:
if rt[0] == '!trap': if rt[0] == '!trap':
@ -172,7 +172,7 @@ class TransmitterManager(BaseTransmitter, metaclass=ABCMeta):
def _exec_cmd_iter(self, cmd): def _exec_cmd_iter(self, cmd):
assert isinstance(cmd, list) assert isinstance(cmd, list)
result_iter = self.ar.talk_iter(cmd)
result_iter = '<not request>'#self.ar.talk_iter(cmd)
for rt in result_iter: for rt in result_iter:
if len(rt) < 2: if len(rt) < 2:
continue continue
@ -377,10 +377,12 @@ class IpAddressListManager(TransmitterManager, metaclass=ABCMeta):
class MikrotikTransmitter(QueueManager, IpAddressListManager): class MikrotikTransmitter(QueueManager, IpAddressListManager):
def add_user_range(self, user_list): def add_user_range(self, user_list):
super(MikrotikTransmitter, self).add_user_range(user_list)
for usr in user_list: for usr in user_list:
self.add_user(usr) self.add_user(usr)
def remove_user_range(self, users): def remove_user_range(self, users):
super(MikrotikTransmitter, self).remove_user_range(users)
queue_ids = [usr.queue_id for usr in users if usr is not None] queue_ids = [usr.queue_id for usr in users if usr is not None]
QueueManager.remove_range(self, queue_ids) QueueManager.remove_range(self, queue_ids)
ips = [user.ip for user in users if isinstance(user, AbonStruct)] ips = [user.ip for user in users if isinstance(user, AbonStruct)]
@ -390,6 +392,7 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager):
IpAddressListManager.remove(self, ip_list_entity[0]['=.id']) IpAddressListManager.remove(self, ip_list_entity[0]['=.id'])
def add_user(self, user, ip_timeout=None): def add_user(self, user, ip_timeout=None):
super(MikrotikTransmitter, self).add_user(user)
assert isinstance(user.ip, IpStruct) assert isinstance(user.ip, IpStruct)
if user.tariff is None or not isinstance(user.tariff, TariffStruct): if user.tariff is None or not isinstance(user.tariff, TariffStruct):
return return
@ -401,6 +404,7 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager):
IpAddressListManager.remove(self, firewall_ip_list_obj[0]['=.id']) IpAddressListManager.remove(self, firewall_ip_list_obj[0]['=.id'])
def remove_user(self, user): def remove_user(self, user):
super(MikrotikTransmitter, self).remove_user(user)
QueueManager.remove(self, user) QueueManager.remove(self, user)
firewall_ip_list_obj = IpAddressListManager.find(self, user.ip, LIST_USERS_ALLOWED) firewall_ip_list_obj = IpAddressListManager.find(self, user.ip, LIST_USERS_ALLOWED)
if firewall_ip_list_obj is not None and len(firewall_ip_list_obj) > 1: if firewall_ip_list_obj is not None and len(firewall_ip_list_obj) > 1:
@ -408,6 +412,7 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager):
# обновляем основную инфу абонента # обновляем основную инфу абонента
def update_user(self, user, ip_timeout=None): def update_user(self, user, ip_timeout=None):
super(MikrotikTransmitter, self).update_user(user)
assert isinstance(user.ip, IpStruct) assert isinstance(user.ip, IpStruct)
# ищем ip абонента в списке ip # ищем ip абонента в списке ip
@ -446,6 +451,7 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager):
QueueManager.update(self, user) QueueManager.update(self, user)
def ping(self, host, count=10): def ping(self, host, count=10):
super(MikrotikTransmitter, self).ping(host)
r = self._exec_cmd([ r = self._exec_cmd([
'/ip/arp/print', '/ip/arp/print',
'?address=%s' % host '?address=%s' % host
@ -460,14 +466,6 @@ class MikrotikTransmitter(QueueManager, IpAddressListManager):
received, sent = int(r[-2:][0]['=received']), int(r[-2:][0]['=sent']) received, sent = int(r[-2:][0]['=received']), int(r[-2:][0]['=sent'])
return received, sent return received, sent
# приостановливаем обслуживание абонента
def pause_user(self, user):
self.remove_user(user)
# продолжаем обслуживание абонента
def start_user(self, user):
pass
# Тарифы хранить нам не надо, так что методы тарифов ниже не реализуем # Тарифы хранить нам не надо, так что методы тарифов ниже не реализуем
def add_tariff_range(self, tariff_list): def add_tariff_range(self, tariff_list):
pass pass

1
agent/settings.py.example

@ -10,6 +10,7 @@ KEYFILE = "/etc/ssl/server.key"
# Использовать-ли при передаче инфы между NAS и основным сервером SSL # Использовать-ли при передаче инфы между NAS и основным сервером SSL
IS_USE_SSL = False IS_USE_SSL = False
# Адрес, порт, логин и пароль сервера NAS
NAS_IP = '<NAS IP>' NAS_IP = '<NAS IP>'
NAS_LOGIN = 'admin' NAS_LOGIN = 'admin'
NAS_PASSW = '<PASSWORD>' NAS_PASSW = '<PASSWORD>'

6
bugs.txt

@ -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
!!! Удалить всё что связано с активацией услуги
!!! Убрать досрочное завершение услуги
! Проверить дату завершения услуги ! Проверить дату завершения услуги

3
chatbot/telebot.py

@ -8,8 +8,9 @@ from urllib3.exceptions import ProtocolError
from .models import TelegramBot, ChatException from .models import TelegramBot, ChatException
from chatbot.models import MessageHistory from chatbot.models import MessageHistory
from accounts_app.models import UserProfile from accounts_app.models import UserProfile
from django.conf import settings
token = '285129725:AAF9Si5_b1n1_cN3vJtwXt0gkgsqKBptut4'
token = getattr(settings, 'TELEGRAM_BOT_TOKEN')
class DjingTelebot(helper.ChatHandler): class DjingTelebot(helper.ChatHandler):

10
devapp/dev_types.py

@ -179,9 +179,9 @@ class OnuDevice(DevBase, SNMPBaseWorker):
class EltexPort(BasePort): class EltexPort(BasePort):
def __init__(self, num, name, status, mac, speed, snmpWorker):
BasePort.__init__(self, num, name, status, mac, speed)
assert issubclass(snmpWorker.__class__ , SNMPBaseWorker)
def __init__(self, snmpWorker, *args, **kwargs):
BasePort.__init__(self, *args, **kwargs)
assert issubclass(snmpWorker.__class__, SNMPBaseWorker)
self.snmp_worker = snmpWorker self.snmp_worker = snmpWorker
# выключаем этот порт # выключаем этот порт
@ -213,13 +213,13 @@ class EltexSwitch(DLinkDevice):
speeds = self.get_list('.1.3.6.1.2.1.31.1.1.1.15') speeds = self.get_list('.1.3.6.1.2.1.31.1.1.1.15')
res = [] res = []
for n in range(28): for n in range(28):
res.append(EltexPort(
res.append(EltexPort(self,
n+1, n+1,
'',#nams[n] if len(nams) > 0 else _('does not fetch the name'), '',#nams[n] if len(nams) > 0 else _('does not fetch the name'),
True if int(stats[n]) == 1 else False, True if int(stats[n]) == 1 else False,
'',#macs[n] if len(macs) > 0 else _('does not fetch the mac'), '',#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, int(speeds[n]) if len(speeds) > 0 and int(oper_stats[n]) == 1 else 0,
self))
))
return res return res
def get_device_name(self): def get_device_name(self):

2
djing/settings_example.py

@ -166,3 +166,5 @@ DIALING_MEDIA = 'path/to/asterisk_records'
DHCP_TIMEOUT = 14400 DHCP_TIMEOUT = 14400
DEFAULT_SNMP_PASSWORD = 'public' DEFAULT_SNMP_PASSWORD = 'public'
TELEGRAM_BOT_TOKEN = 'bot token'

2
docs/bot.md

@ -0,0 +1,2 @@
# Оповещения из биллнга
Мгновенная связь с администратором и общение с сотрудниками.

286
docs/dev.md

@ -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
```

62
docs/install.md

@ -1,19 +1,49 @@
## Установка(не завершил описание):
## Установка:
Работа предполагается на python3. Работа предполагается на python3.
Я предпочитаю запускать wsgi сервер на связке uWSGI + Nginx, так что ставить будем соответствующие пакеты. Я предпочитаю запускать wsgi сервер на связке uWSGI + Nginx, так что ставить будем соответствующие пакеты.
На ArchLinux нужые пакеты можно установить так:
#####На Fedora25 нужные пакеты можно установить так:
Для начала подготовим систему, очистим и обновим пакеты. Процесс обновления долгий, так что можно пойти заварить себе чай :)
``` ```
# pacman -Sy mariadb-clients python3 python-pip nginx uwsgi
# dnf clean all
# dnf -y update
``` ```
Дальше ставим всё для python через pip:
Затем установим зависимости
``` ```
# pip install git+https://github.com/nerosketch/djing.git
# dnf -y install python3 python3-devel python3-pip python3-pillow mariadb uwsgi nginx redis net-snmp net-snmp-libs net-snmp-utils net-snmp-devel net-snmp-python git redhat-rpm-config
``` ```
### Настройка WEB Сервера
Условимся что путь к папке с проектом находится по адресу: </var/www/djing>
Условимся что путь к папке с проектом находится по адресу: */var/www/djing*.
Дальше создадим каталок для web, затем обновляем pip и ставим проект через pip:
```
# mkdir /vaw/www
# cd /var/www
# pip3 install --upgrade pip
# git clone https://github.com/nerosketch/djing.git
# pip3 install -r djing/requirements.txt
```
Скопируем конфиги из примеров в реальные:
```
cd /var/www/djing
cp djing/settings_example.py djing/settings.py
cp agent/settings.py.example agent/settings.py
```
Затем отредактируйте конфиги для своих нужд.
Для удобства я создаю пользователя и группу http:http, и всё что связано с web-сервером запускаю от имени http.
```
groupadd -r http
useradd -l -M -r -d /dev/null -g http -s /sbin/nologin http
chown -R http:http /var/www
chown -R http:http /etc/nginx
chown -R http:http /etc/uwsgi.*
```
### Настройка WEB Сервера
Конфиг Nginx на моём рабочем сервере выглядит так: Конфиг Nginx на моём рабочем сервере выглядит так:
user http; user http;
@ -75,6 +105,20 @@
У меня конфиг лежит по адресу /etc/uwsgi.ini У меня конфиг лежит по адресу /etc/uwsgi.ini
### Настраиваем системные утилиты
### Настраиваем демоны
Если ваша система работает с поддержкой *systemd* то в каталоге *systemd_units* проекта вы найдёте юниты для systemd. Если ваша система работает с поддержкой *systemd* то в каталоге *systemd_units* проекта вы найдёте юниты для systemd.
Скопируйте их в каталог юнитов systemd
Скопируйте их в каталог юнитов systemd, у меня это путь */etc/systemd/system*.
__Настоятельно рекомендую заглянуть внутрь этих юнитов__. Проверте пути исполняемых файлов, права и прочее.
А теперь включим и запустим нужные демоны
```
# systemctl daemon-reload
# systemctl enable djing_queue.service
# systemctl start djing_queue.service
# systemctl enable djing_rotate.timer
# systemctl start djing_rotate.timer
# systemctl enable djing_telebot.service
# systemctl start djing_telebot.service
```
Перед включением юнита *djing_telebot.service* создайте Telegram бота и впишите в файл *djing/settings.py* в переменную *TELEGRAM_BOT_TOKEN* токен вашего бота.
С помощью этого бота вы будете получать различные сообщения из биллинга. Подробнее в инструкции к [модулю оповещений](./docs/bot.md).

23
docs/netflow.md

@ -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).
Посмотреть пример работы можно так:

2
requirements.txt

@ -5,7 +5,7 @@ telepot
netaddr netaddr
# for testing required xmltodict # for testing required xmltodict
xmltodict xmltodict
mysqlclient
PyMySQL
easysnmp easysnmp
rq rq
pid pid

3
setup.py

@ -0,0 +1,3 @@
#!/usr/bin/env python3
import os
import sys

51
templates/toolbar_page.html

@ -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="#">&laquo;</a></li>
{% else %}
<li><a href="?{{ url }}{{ start_divide }}p=1">&laquo;</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="#">&raquo;</a></li>
{% else %}
<li><a href="?{{ url }}{{ start_divide }}p={{ pag.paginator.num_pages }}">&raquo;</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="#">&laquo;</a></li>
{% else %}
<li><a href="?{% url_page_replace request 'p' 1 %}">&laquo;</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="#">&raquo;</a></li>
{% else %}
<li><a href="?{% url_page_replace request 'p' pag.paginator.num_pages %}">&raquo;</a></li>
{% endif %}
</ul>
</div> </div>
{% endif %}
{% endwith %}
{% endwith %}
</div>
Loading…
Cancel
Save