Browse Source

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

devel
Dmitry Novikov 9 years ago
parent
commit
5ee1a3972f
  1. 2
      abonapp/forms.py
  2. 23
      abonapp/models.py
  3. 4
      abonapp/pay_systems.py
  4. 12
      accounts_app/models.py
  5. 4
      agent/core.py
  6. 51
      agent/mod_mikrotik.py
  7. 18
      agent/structs.py
  8. 3
      chatbot/locale/ru/LC_MESSAGES/django.po
  9. 4
      chatbot/models.py
  10. 11
      chatbot/telebot.py
  11. 2
      cron.py
  12. 9
      devapp/dev_types.py
  13. 3
      devapp/tests.py
  14. 4
      devapp/views.py
  15. 2
      djing/settings_example.py
  16. 0
      docs/ats.md
  17. 3
      docs/dev.md
  18. BIN
      docs/img/login.png
  19. 294
      docs/install.md
  20. 9
      mydefs.py
  21. 7
      photo_app/models.py
  22. 7
      requirements.txt
  23. 3
      setup.py
  24. 3
      tariff_app/models.py
  25. 2
      templates/toolbar_page.html

2
abonapp/forms.py

@ -70,7 +70,7 @@ class AbonForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
raw_password = self.cleaned_data['password'] raw_password = self.cleaned_data['password']
acc = super().save(commit=False)
acc = super(AbonForm, self).save(commit=False)
acc.password = make_password(raw_password) acc.password = make_password(raw_password)
if commit: if commit:
acc.save() acc.save()

23
abonapp/models.py

@ -164,7 +164,8 @@ class Abon(UserProfile):
# покупаем тариф # покупаем тариф
def pick_tariff(self, tariff, author, comment=None, deadline=None): def pick_tariff(self, tariff, author, comment=None, deadline=None):
assert isinstance(tariff, Tariff)
if not isinstance(tariff, Tariff):
raise TypeError
amount = round(tariff.amount, 2) amount = round(tariff.amount, 2)
@ -246,7 +247,7 @@ class PassportInfo(models.Model):
date_of_acceptance = models.DateField() date_of_acceptance = models.DateField()
abon = models.OneToOneField(Abon, on_delete=models.SET_NULL, blank=True, null=True) abon = models.OneToOneField(Abon, on_delete=models.SET_NULL, blank=True, null=True)
def __unicode__(self):
def __str__(self):
return "%s %s" % (self.series, self.number) return "%s %s" % (self.series, self.number)
@ -330,7 +331,7 @@ def abon_post_save(sender, instance, **kwargs):
# обновляем абонента на NAS # обновляем абонента на NAS
tm.update_user(agent_abon, ip_timeout=timeout) tm.update_user(agent_abon, ip_timeout=timeout)
except (NasFailedResult, NasNetworkError) as e:
except (NasFailedResult, NasNetworkError, ConnectionResetError) as e:
print('ERROR:', e) print('ERROR:', e)
return True return True
@ -356,6 +357,22 @@ def abon_tariff_post_init(sender, instance, **kwargs):
instance.deadline = calc_obj.calc_deadline() instance.deadline = calc_obj.calc_deadline()
def abontariff_pre_delete(sender, instance, **kwargs):
try:
abon = Abon.objects.get(current_tariff=instance)
ab = abon.build_agent_struct()
if ab is None:
return True
tm = Transmitter()
tm.remove_user(ab)
except Abon.DoesNotExist:
print('ERROR: Abon.DoesNotExist')
except (NasFailedResult, NasNetworkError, ConnectionResetError) as e:
print('NetErr:', e)
return True
models.signals.post_save.connect(abon_post_save, sender=Abon) models.signals.post_save.connect(abon_post_save, sender=Abon)
models.signals.post_delete.connect(abon_del_signal, sender=Abon) models.signals.post_delete.connect(abon_del_signal, sender=Abon)
models.signals.post_init.connect(abon_tariff_post_init, sender=AbonTariff) models.signals.post_init.connect(abon_tariff_post_init, sender=AbonTariff)
models.signals.pre_delete.connect(abontariff_pre_delete, sender=AbonTariff)

4
abonapp/pay_systems.py

@ -3,9 +3,11 @@ from django.utils import timezone
from mydefs import safe_int, safe_float from mydefs import safe_int, safe_float
from .models import Abon, AllTimePayLog from .models import Abon, AllTimePayLog
from django.db import DatabaseError from django.db import DatabaseError
from django.conf import settings
from djing.settings import pay_SECRET as SECRET, pay_SERV_ID as SERV_ID
SECRET = getattr(settings, 'pay_SECRET')
SERV_ID = getattr(settings, 'pay_SERV_ID')
#?ACT=1&PAY_ACCOUNT=960849&SERVICE_ID=y832r92y8f9e&PAY_ID=3561234&TRADE_POINT=377&SIGN=32e533a72389fe4e93746509f9d672f8 #?ACT=1&PAY_ACCOUNT=960849&SERVICE_ID=y832r92y8f9e&PAY_ID=3561234&TRADE_POINT=377&SIGN=32e533a72389fe4e93746509f9d672f8

12
accounts_app/models.py

@ -3,11 +3,14 @@ from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from djing.settings import DEFAULT_PICTURE
from django.conf import settings
from photo_app.models import Photo from photo_app.models import Photo
DEFAULT_PICTURE = getattr(settings, 'DEFAULT_PICTURE', '/static/images/default-avatar.png')
TELEPHONE_REGEXP = getattr(settings, 'TELEPHONE_REGEXP', r'^\+[7,8,9,3]\d{10,11}$')
class MyUserManager(BaseUserManager): class MyUserManager(BaseUserManager):
def create_user(self, telephone, username, password=None): def create_user(self, telephone, username, password=None):
""" """
@ -51,7 +54,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
max_length=16, max_length=16,
verbose_name=_('Telephone'), verbose_name=_('Telephone'),
#unique=True, #unique=True,
validators=[RegexValidator('^\+[7,8,9,3]\d{10,11}$')]
validators=[RegexValidator(TELEPHONE_REGEXP)]
) )
avatar = models.ForeignKey(Photo, null=True, blank=True, on_delete=models.SET_NULL) avatar = models.ForeignKey(Photo, null=True, blank=True, on_delete=models.SET_NULL)
email = models.EmailField(default='admin@example.ru') email = models.EmailField(default='admin@example.ru')
@ -65,13 +68,12 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
def get_short_name(self): def get_short_name(self):
return self.username or self.telephone return self.username or self.telephone
# Use UserManager to get the create_user method, etc. # Use UserManager to get the create_user method, etc.
objects = MyUserManager() objects = MyUserManager()
@property @property
def is_staff(self): def is_staff(self):
"Is the user a member of staff?"
" Is the user a member of staff?"
# Simplest possible answer: All admins are staff # Simplest possible answer: All admins are staff
return self.is_admin return self.is_admin

4
agent/core.py

@ -19,8 +19,6 @@ class NasNetworkError(Exception):
def check_input_type(*types): def check_input_type(*types):
def real_check(fn): def real_check(fn):
def wrapped(self, *args): 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): for param_type, param in zip(types, args):
if not isinstance(param, param_type): if not isinstance(param, param_type):
raise TypeError("%s must be %s, but is %s" % (str(param), str(param_type), type(param))) raise TypeError("%s must be %s, but is %s" % (str(param), str(param_type), type(param)))
@ -97,7 +95,7 @@ class BaseTransmitter(metaclass=ABCMeta):
""" """
@abstractmethod @abstractmethod
@check_input_type(str)
@check_input_type(str, int)
def ping(self, host, count=10): def ping(self, host, count=10):
""" """
:param host: ip адрес в текстовом виде, например '192.168.0.1' :param host: ip адрес в текстовом виде, например '192.168.0.1'

51
agent/mod_mikrotik.py

@ -6,12 +6,12 @@ from hashlib import md5
from .core import BaseTransmitter, NasFailedResult, NasNetworkError from .core import BaseTransmitter, NasFailedResult, NasNetworkError
from mydefs import ping from mydefs import ping
from .structs import TariffStruct, AbonStruct, IpStruct from .structs import TariffStruct, AbonStruct, IpStruct
from . import settings
from djing.settings import DEBUG
from . import settings as local_settings
from django.conf import settings
import re import re
#DEBUG=True
DEBUG = getattr(settings, 'DEBUG', False)
LIST_USERS_ALLOWED = 'DjingUsersAllowed' LIST_USERS_ALLOWED = 'DjingUsersAllowed'
LIST_USERS_BLOCKED = 'DjingUsersBlocked' LIST_USERS_BLOCKED = 'DjingUsersBlocked'
@ -144,15 +144,17 @@ class ApiRos:
class TransmitterManager(BaseTransmitter, metaclass=ABCMeta): class TransmitterManager(BaseTransmitter, metaclass=ABCMeta):
def __init__(self, login=None, password=None, ip=None, port=None): def __init__(self, login=None, password=None, ip=None, port=None):
ip = ip or settings.NAS_IP
ip = ip or getattr(local_settings, 'NAS_IP')
if ip is None:
raise NasNetworkError('Не передан ip адрес NAS')
if not ping(ip): if not ping(ip):
raise NasNetworkError('NAS %s не пингуется' % ip) raise NasNetworkError('NAS %s не пингуется' % ip)
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port or settings.NAS_PORT))
s.connect((ip, port or getattr(local_settings, 'NAS_PORT', 8728)))
self.s = s self.s = s
self.ar = ApiRos(s) self.ar = ApiRos(s)
self.ar.login(login or settings.NAS_LOGIN, password or settings.NAS_PASSW)
self.ar.login(login or getattr(local_settings, 'NAS_LOGIN'), password or getattr(local_settings, 'NAS_PASSW'))
except ConnectionRefusedError: except ConnectionRefusedError:
raise NasNetworkError('Подключение к %s отклонено (Connection Refused)' % ip) raise NasNetworkError('Подключение к %s отклонено (Connection Refused)' % ip)
@ -161,7 +163,8 @@ class TransmitterManager(BaseTransmitter, metaclass=ABCMeta):
self.s.close() self.s.close()
def _exec_cmd(self, cmd): def _exec_cmd(self, cmd):
assert isinstance(cmd, list)
if not isinstance(cmd, list):
raise TypeError
result_iter = self.ar.talk_iter(cmd) result_iter = self.ar.talk_iter(cmd)
res = [] res = []
for rt in result_iter: for rt in result_iter:
@ -171,7 +174,8 @@ class TransmitterManager(BaseTransmitter, metaclass=ABCMeta):
return res return res
def _exec_cmd_iter(self, cmd): def _exec_cmd_iter(self, cmd):
assert isinstance(cmd, list)
if not isinstance(cmd, list):
raise TypeError
result_iter = self.ar.talk_iter(cmd) result_iter = self.ar.talk_iter(cmd)
for rt in result_iter: for rt in result_iter:
if len(rt) < 2: if len(rt) < 2:
@ -223,7 +227,8 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta):
return self._build_shape_obj(ret[0]) return self._build_shape_obj(ret[0])
def add(self, user): def add(self, user):
assert isinstance(user, AbonStruct)
if not isinstance(user, AbonStruct):
raise TypeError
if user.tariff is None or not isinstance(user.tariff, TariffStruct): if user.tariff is None or not isinstance(user.tariff, TariffStruct):
return return
return self._exec_cmd(['/queue/simple/add', return self._exec_cmd(['/queue/simple/add',
@ -236,7 +241,8 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta):
]) ])
def remove(self, user): def remove(self, user):
assert isinstance(user, AbonStruct)
if not isinstance(user, AbonStruct):
raise TypeError
q = self.find('uid%d' % user.uid) q = self.find('uid%d' % user.uid)
if q is not None: if q is not None:
return self._exec_cmd(['/queue/simple/remove', '=.id=' + getattr(q, 'queue_id', '')]) return self._exec_cmd(['/queue/simple/remove', '=.id=' + getattr(q, 'queue_id', '')])
@ -246,7 +252,8 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta):
return self._exec_cmd(['/queue/simple/remove', '=numbers=' + ','.join(q_ids)]) return self._exec_cmd(['/queue/simple/remove', '=numbers=' + ','.join(q_ids)])
def update(self, user): def update(self, user):
assert isinstance(user, AbonStruct)
if not isinstance(user, AbonStruct):
raise TypeError
if user.tariff is None or not isinstance(user.tariff, TariffStruct): if user.tariff is None or not isinstance(user.tariff, TariffStruct):
return return
queue = self.find('uid%d' % user.uid) queue = self.find('uid%d' % user.uid)
@ -282,7 +289,8 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta):
yield int(queue[1]['=.id'].replace('*', ''), base=16) yield int(queue[1]['=.id'].replace('*', ''), base=16)
def disable(self, user): def disable(self, user):
assert isinstance(user, AbonStruct)
if not isinstance(user, AbonStruct):
raise TypeError
q = self.find('uid%d' % user.uid) q = self.find('uid%d' % user.uid)
if q is None: if q is None:
self.add(user) self.add(user)
@ -291,7 +299,8 @@ class QueueManager(TransmitterManager, metaclass=ABCMeta):
return self._exec_cmd(['/queue/simple/disable', '=.id=*' + getattr(q, 'queue_id', '')]) return self._exec_cmd(['/queue/simple/disable', '=.id=*' + getattr(q, 'queue_id', '')])
def enable(self, user): def enable(self, user):
assert isinstance(user, AbonStruct)
if not isinstance(user, AbonStruct):
raise TypeError
q = self.find('uid%d' % user.uid) q = self.find('uid%d' % user.uid)
if q is None: if q is None:
self.add(user) self.add(user)
@ -309,7 +318,8 @@ class IpAddressListObj(IpStruct):
class IpAddressListManager(TransmitterManager, metaclass=ABCMeta): class IpAddressListManager(TransmitterManager, metaclass=ABCMeta):
def add(self, list_name, ip, timeout=None): def add(self, list_name, ip, timeout=None):
assert isinstance(ip, IpStruct)
if not isinstance(ip, IpStruct):
raise TypeError
commands = [ commands = [
'/ip/firewall/address-list/add', '/ip/firewall/address-list/add',
'=list=%s' % list_name, '=list=%s' % list_name,
@ -342,7 +352,8 @@ class IpAddressListManager(TransmitterManager, metaclass=ABCMeta):
]) ])
def find(self, ip, list_name): def find(self, ip, list_name):
assert isinstance(ip, IpStruct)
if not isinstance(ip, IpStruct):
raise TypeError
return self._exec_cmd([ return self._exec_cmd([
'/ip/firewall/address-list/print', 'where', '/ip/firewall/address-list/print', 'where',
'?list=%s' % list_name, '?list=%s' % list_name,
@ -392,8 +403,9 @@ 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)
super(MikrotikTransmitter, self).add_user(user, ip_timeout)
if not isinstance(user.ip, IpStruct):
raise TypeError
if user.tariff is None or not isinstance(user.tariff, TariffStruct): if user.tariff is None or not isinstance(user.tariff, TariffStruct):
return return
QueueManager.add(self, user) QueueManager.add(self, user)
@ -412,8 +424,9 @@ 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)
super(MikrotikTransmitter, self).update_user(user, ip_timeout)
if not isinstance(user.ip, IpStruct):
raise TypeError
# ищем ip абонента в списке ip # ищем ip абонента в списке ip
find_res = IpAddressListManager.find(self, user.ip, LIST_USERS_ALLOWED) find_res = IpAddressListManager.find(self, user.ip, LIST_USERS_ALLOWED)

18
agent/structs.py

@ -38,7 +38,8 @@ class IpStruct(BaseStruct):
return self.__ip return self.__ip
def __eq__(self, other): def __eq__(self, other):
assert isinstance(other, IpStruct)
if not isinstance(other, IpStruct):
raise TypeError('Instance must be IpStruct')
return self.__ip == other.__ip return self.__ip == other.__ip
def __int__(self): def __int__(self):
@ -100,8 +101,10 @@ class AbonStruct(BaseStruct):
def serialize(self): def serialize(self):
if self.tariff is None: if self.tariff is None:
return return
assert isinstance(self.tariff, TariffStruct)
assert isinstance(self.ip, IpStruct)
if not isinstance(self.tariff, TariffStruct):
raise TypeError('Instance must be TariffStruct')
if not isinstance(self.ip, IpStruct):
raise TypeError('Instance must be IpStruct')
dt = pack("!LII?", self.uid, int(self.ip), self.tariff.tid, self.is_active) dt = pack("!LII?", self.uid, int(self.ip), self.tariff.tid, self.is_active)
return dt return dt
@ -110,13 +113,15 @@ class AbonStruct(BaseStruct):
self.uid = dt[0] self.uid = dt[0]
self.ip = IpStruct(dt[1]) self.ip = IpStruct(dt[1])
if tariff is not None: if tariff is not None:
assert isinstance(tariff, TariffStruct)
if not isinstance(tariff, TariffStruct):
raise TypeError
self.tariff = tariff self.tariff = tariff
self.is_active = dt['3'] self.is_active = dt['3']
return self return self
def __eq__(self, other): def __eq__(self, other):
assert isinstance(other, AbonStruct)
if not isinstance(other, AbonStruct):
raise TypeError
r = self.uid == other.uid and self.ip == other.ip r = self.uid == other.uid and self.ip == other.ip
r = r and self.tariff == other.tariff r = r and self.tariff == other.tariff
return r return r
@ -147,5 +152,6 @@ class ShapeItem(BaseStruct):
return self return self
def __eq__(self, other): def __eq__(self, other):
assert isinstance(other, ShapeItem)
if not isinstance(other, ShapeItem):
raise TypeError
return self.sid == other.sid and self.abon == other.abon return self.sid == other.sid and self.abon == other.abon

3
chatbot/locale/ru/LC_MESSAGES/django.po

@ -55,3 +55,6 @@ msgstr "Давай пинганём, напиши ip. Нужно будет по
msgid "Yes, it's nice to meet% s, I will notify you about events in billing. Successful work;)" msgid "Yes, it's nice to meet% s, I will notify you about events in billing. Successful work;)"
msgstr "Да, приятно познакомиться %s, я буду оповещать тебя о событиях в биллинге. Удачной работы ;)" msgstr "Да, приятно познакомиться %s, я буду оповещать тебя о событиях в биллинге. Удачной работы ;)"
msgid "Telegram bot token not found"
msgstr "Токен для бота Telegram не найден"

4
chatbot/models.py

@ -1,5 +1,7 @@
from django.db import models from django.db import models
from djing.settings import AUTH_USER_MODEL
from django.conf import settings
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL')
class ChatException(Exception): class ChatException(Exception):

11
chatbot/telebot.py

@ -31,7 +31,8 @@ class DjingTelebot(helper.ChatHandler):
# задаём вопрос пользователю, и ожидаем ответ в fn # задаём вопрос пользователю, и ожидаем ответ в fn
def _question(self, text, fn): def _question(self, text, fn):
assert isinstance(fn, collections.Callable)
if not isinstance(fn, collections.Callable):
raise TypeError
self._dialog_fn = fn self._dialog_fn = fn
if text is not None: if text is not None:
self._sent_reply(text) self._sent_reply(text)
@ -75,7 +76,8 @@ class DjingTelebot(helper.ChatHandler):
if text in list(self.cmds.keys()): if text in list(self.cmds.keys()):
self.cmds[text]() self.cmds[text]()
elif self._dialog_fn is not None: elif self._dialog_fn is not None:
assert callable(self._dialog_fn)
if not callable(self._dialog_fn):
raise TypeError
self._dialog_fn(text) self._dialog_fn(text)
self._dialog_fn = None self._dialog_fn = None
else: else:
@ -92,7 +94,8 @@ class DjingTelebot(helper.ChatHandler):
try: try:
TelegramBot.objects.get(user=profile) TelegramBot.objects.get(user=profile)
except TelegramBot.DoesNotExist: except TelegramBot.DoesNotExist:
assert self._chat_id != 0
if self._chat_id == 0:
raise ChatException('telebot.py. def question_name: Chat id is empty')
TelegramBot.objects.create( TelegramBot.objects.create(
user=profile, user=profile,
chat_id=self._chat_id chat_id=self._chat_id
@ -130,6 +133,8 @@ class DjingTelebot(helper.ChatHandler):
# Просто отправляем текст оповещения указанному админу # Просто отправляем текст оповещения указанному админу
def send_notify(msg_text, account): def send_notify(msg_text, account):
try: try:
if token is None:
raise ChatException(_('Telegram bot token not found'))
tb = TelegramBot.objects.get(user=account) tb = TelegramBot.objects.get(user=account)
tbot = Bot(token) tbot = Bot(token)
tbot.sendMessage(tb.chat_id, msg_text) tbot.sendMessage(tb.chat_id, msg_text)

2
cron.py

@ -13,7 +13,7 @@ def main():
users = Abon.objects.all() users = Abon.objects.all()
for user in users: for user in users:
try: try:
# бдим за услугами абонента: просроченные отключить, заказанные подключить
# бдим за услугами абонента
user.bill_service(user) user.bill_service(user)
# если нет ip то и нет смысла лезть в NAS # если нет ip то и нет смысла лезть в NAS

9
devapp/dev_types.py

@ -9,7 +9,8 @@ class DLinkPort(BasePort):
def __init__(self, num, name, status, mac, speed, snmpWorker): def __init__(self, num, name, status, mac, speed, snmpWorker):
BasePort.__init__(self, num, name, status, mac, speed) BasePort.__init__(self, num, name, status, mac, speed)
assert issubclass(snmpWorker.__class__ , SNMPBaseWorker)
if not issubclass(snmpWorker.__class__ , SNMPBaseWorker):
raise TypeError
self.snmp_worker = snmpWorker self.snmp_worker = snmpWorker
# выключаем этот порт # выключаем этот порт
@ -79,7 +80,8 @@ class DLinkDevice(DevBase, SNMPBaseWorker):
class ONUdev(BasePort): class ONUdev(BasePort):
def __init__(self, num, name, status, mac, speed, signal, snmpWorker): def __init__(self, num, name, status, mac, speed, signal, snmpWorker):
super(ONUdev, self).__init__(num, name, status, mac, speed) super(ONUdev, self).__init__(num, name, status, mac, speed)
assert issubclass(snmpWorker.__class__, SNMPBaseWorker)
if not issubclass(snmpWorker.__class__, SNMPBaseWorker):
raise TypeError
self.snmp_worker = snmpWorker self.snmp_worker = snmpWorker
self.signal = signal self.signal = signal
@ -181,7 +183,8 @@ class EltexPort(BasePort):
def __init__(self, snmpWorker, *args, **kwargs): def __init__(self, snmpWorker, *args, **kwargs):
BasePort.__init__(self, *args, **kwargs) BasePort.__init__(self, *args, **kwargs)
assert issubclass(snmpWorker.__class__, SNMPBaseWorker)
if not issubclass(snmpWorker.__class__, SNMPBaseWorker):
raise TypeError
self.snmp_worker = snmpWorker self.snmp_worker = snmpWorker
# выключаем этот порт # выключаем этот порт

3
devapp/tests.py

@ -14,7 +14,8 @@ class DevTest(TestCase):
ports = dev.get_ports() ports = dev.get_ports()
print('gports') print('gports')
for port in ports: for port in ports:
assert issubclass(port.__class__, dev_types.BasePort)
if not issubclass(port.__class__, dev_types.BasePort):
raise TypeError
print(('\tPort:', port.nm, port.st, port.mac(), port.sp)) print(('\tPort:', port.nm, port.st, port.mac(), port.sp))
# Disable 2 port # Disable 2 port
print((ports[1].disable())) print((ports[1].disable()))

4
devapp/views.py

@ -14,7 +14,7 @@ from .models import Device, Port, DeviceDBException
from mydefs import pag_mn, res_success, res_error, only_admins, ping, order_helper from mydefs import pag_mn, res_success, res_error, only_admins, ping, order_helper
from .forms import DeviceForm, PortForm from .forms import DeviceForm, PortForm
from abonapp.models import AbonGroup, Abon from abonapp.models import AbonGroup, Abon
from djing.settings import DEFAULT_SNMP_PASSWORD
from django.conf import settings
@login_required @login_required
@ -103,7 +103,7 @@ def dev(request, grp, devid=0):
'mac_addr': request.GET.get('mac'), 'mac_addr': request.GET.get('mac'),
'comment': request.GET.get('c'), 'comment': request.GET.get('c'),
'ip_address': request.GET.get('ip'), 'ip_address': request.GET.get('ip'),
'man_passw': DEFAULT_SNMP_PASSWORD
'man_passw': getattr(settings, 'DEFAULT_SNMP_PASSWORD', '')
}) })
else: else:
frm = DeviceForm(instance=devinst) frm = DeviceForm(instance=devinst)

2
djing/settings_example.py

@ -168,3 +168,5 @@ DHCP_TIMEOUT = 14400
DEFAULT_SNMP_PASSWORD = 'public' DEFAULT_SNMP_PASSWORD = 'public'
TELEGRAM_BOT_TOKEN = 'bot token' TELEGRAM_BOT_TOKEN = 'bot token'
TELEPHONE_REGEXP = r'^\+[7,8,9,3]\d{10,11}$'

0
docs/ats.md

3
docs/dev.md

@ -12,7 +12,8 @@ class EltexPort(BasePort):
def __init__(self, snmpWorker, *args, **kwargs): def __init__(self, snmpWorker, *args, **kwargs):
BasePort.__init__(self, *args, **kwargs) BasePort.__init__(self, *args, **kwargs)
assert issubclass(snmpWorker.__class__, SNMPBaseWorker)
if not issubclass(snmpWorker.__class__, SNMPBaseWorker):
raise TypeError
self.snmp_worker = snmpWorker self.snmp_worker = snmpWorker
# выключаем этот порт # выключаем этот порт

BIN
docs/img/login.png

After

Width: 1239  |  Height: 809  |  Size: 14 KiB

294
docs/install.md

@ -2,7 +2,7 @@
Работа предполагается на python3. Работа предполагается на python3.
Я предпочитаю запускать wsgi сервер на связке uWSGI + Nginx, так что ставить будем соответствующие пакеты. Я предпочитаю запускать wsgi сервер на связке uWSGI + Nginx, так что ставить будем соответствующие пакеты.
#####На Fedora25 нужные пакеты можно установить так:
##### На Fedora25 нужные пакеты можно установить так:
Для начала подготовим систему, очистим и обновим пакеты. Процесс обновления долгий, так что можно пойти заварить себе чай :) Для начала подготовим систему, очистим и обновим пакеты. Процесс обновления долгий, так что можно пойти заварить себе чай :)
``` ```
@ -12,13 +12,18 @@
Затем установим зависимости Затем установим зависимости
``` ```
# 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
# dnf -y install python3 python3-devel python3-pip python3-pillow mariadb mariadb-devel uwsgi nginx uwsgi-plugin-python3 redis net-snmp net-snmp-libs net-snmp-utils net-snmp-devel net-snmp-python git redhat-rpm-config
```
Лучше чтоб версия python по умолчанию была третья:
```
# ln -sf python3 /usr/bin/python
``` ```
Условимся что путь к папке с проектом находится по адресу: */var/www/djing*. Условимся что путь к папке с проектом находится по адресу: */var/www/djing*.
Дальше создадим каталок для web, затем обновляем pip и ставим проект через pip: Дальше создадим каталок для web, затем обновляем pip и ставим проект через pip:
``` ```
# mkdir /vaw/www
# mkdir /var/www
# cd /var/www # cd /var/www
# pip3 install --upgrade pip # pip3 install --upgrade pip
# git clone https://github.com/nerosketch/djing.git # git clone https://github.com/nerosketch/djing.git
@ -27,91 +32,254 @@
Скопируем конфиги из примеров в реальные: Скопируем конфиги из примеров в реальные:
``` ```
cd /var/www/djing
cp djing/settings_example.py djing/settings.py
cp agent/settings.py.example agent/settings.py
$ cd /var/www/djing
# cp djing/settings_example.py djing/settings.py
# cp agent/settings.py.example agent/settings.py
``` ```
Затем отредактируйте конфиги для своих нужд. Затем отредактируйте конфиги для своих нужд.
Для удобства я создаю пользователя и группу http:http, и всё что связано с web-сервером запускаю от имени http. Для удобства я создаю пользователя и группу 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.*
# 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.*
# chown -R http:http /run/uwsgi/
``` ```
### Настройка WEB Сервера ### Настройка WEB Сервера
Конфиг Nginx на моём рабочем сервере выглядит так: Конфиг Nginx на моём рабочем сервере выглядит так:
```nginx
user http;
worker_processes auto;
pid /run/nginx.pid;
user http;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
sendfile on;
upstream djing { server unix:///run/uwsgi/djing.sock; }
server {
listen 80;
server_name <ваш-домен>.com;
root /var/www/djing;
charset utf-8;
# укажите где лежит ваш раздел с медиа для сайта
location /media {
alias /var/www/djing/media;
}
# местоположение статики
location /static {
alias /var/www/djing/static;
}
# тут надо указать путь куда у вас установился Django + путь к статике админки
# путь к Django тут: /usr/lib/python3.5/site-packages/django
# путь к статике соответственно: contrib/admin/static/admin
location /static/admin {
alias /usr/lib/python3.5/site-packages/django/contrib/admin/static/admin;
}
# на корневом url / реагируем с помощью сокета проекта
# у нас он называется "djing": upstream djing { server ...
location / {
uwsgi_pass djing;
include uwsgi_params;
}
events {
worker_connections 1024;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream djing { server unix:///run/uwsgi/djing.sock; }
server {
listen 80;
server_name <ваш-домен>.com;
root /var/www/djing;
charset utf-8;
# укажите где лежит ваш раздел с медиа для сайта
location /media {
alias /var/www/djing/media;
}
# местоположение статики
location /static {
alias /var/www/djing/static;
}
# тут надо указать путь куда у вас установился Django + путь к статике админки
# путь к Django тут: /usr/lib/python3.5/site-packages/django
# путь к статике соответственно: contrib/admin/static/admin
location /static/admin {
alias /usr/lib/python3.5/site-packages/django/contrib/admin/static/admin;
}
# на корневом url / реагируем с помощью сокета проекта
# у нас он называется "djing": upstream djing { server ...
location / {
uwsgi_pass djing;
include uwsgi_params;
} }
} }
}
```
Это минимальный конфиг Nginx для работы. Проверте файл /run/uwsgi/djing.sock на доступность пользователю http для чтения. Это минимальный конфиг Nginx для работы. Проверте файл /run/uwsgi/djing.sock на доступность пользователю http для чтения.
Далее настраиваем uWSGI. Мой конфиг для uWSGI в режиме emperor: Далее настраиваем uWSGI. Мой конфиг для uWSGI в режиме emperor:
> /etc/uwsgi.ini
```ini
[uwsgi]
uid = http
gid = http
pidfile = /run/uwsgi/uwsgi.pid
emperor = /etc/uwsgi.d
stats = /run/uwsgi/stats.sock
chmod-socket = 660
emperor-tyrant = true
cap = setgid,setuid
```
Зададим конфиг для *uwsgi vassal*:
> /etc/uwsgi.d/djing.ini
```ini
[uwsgi]
chdir=/var/www/djing/
module=djing.wsgi
master=True
processes=8
;socket=/run/uwsgi/djing.sock
http-socket=:8000
chmod-socket=664
pidfile=/run/uwsgi/django-master.pid
vacuum=True
plugin=python3
```
Примените к созданному файлу пользователя http:
> \# chown http:http /etc/uwsgi.d/djing.ini
Перед пробой запуска отключим все ограничения фаервола:
> \# systemctl stop firewalld
Или даже отключить, если вы отложите настройку *firewalld* на потом:
> \# systemctl disable firewalld
Перед тем как попробовать запустить тестовый сервер скомпилируйте переводы:
> \$ ./manage.py compilemessages -l ru
Попробуем запустить *uwsgi* и djing без Nginx:
> \# uwsgi --gid http --uid http /etc/uwsgi.d/djing.ini
пробуем зайти в биллинг с браузера на <адрес сервера>:8000. Вам должен показаться диалог входа в систему:
![Login screenshot](./img/login.png)
Для того чтоб uwsgi применял к своим файлам пользователя http, надо подредактировать системный юнит uwsgi, у меня он имеет такой путь:
> /usr/lib/systemd/system/uwsgi.service
В нём надо чтоб chown менял пользователя на http, а не на uwsgi:
> ExecStartPre=/bin/chown -r http:http /run/uwsgi
Теперь, если всё прошло успешно, поменяйте в конфиге */etc/uwsgi.d/djing.ini* сокет с http на unix socket:
Раскомментируйте это:
> socket=/run/uwsgi/djing.sock
И закомментируйте эту строку:
> http-socket=:8000
Строка *http-socket=:8000* была для теста, чтоб посмотреть работает-ли uwsgi сам по себе.
[uwsgi]
uid = http
gid = http
pidfile = /run/uwsgi/uwsgi.pid
emperor = /etc/uwsgi.d
stats = /run/uwsgi/stats.sock
chmod-socket = 660
emperor-tyrant = true
cap = setgid,setuid
Теперь можно попробовать запустить *nginx* и *uwsgi*. Ставим в **djing/settings.py** опцию **DEBUG = False**, и пробуем запустить нужные юниты:
У меня конфиг лежит по адресу /etc/uwsgi.ini
> \# systemctl start uwsgi\
> \# systemctl start nginx
По умолчанию на fedora включено SELinux и вы не сможете зайти на сайт пока не настроите его. Для того, чтоб проверить всё
ли правильно мы настроили, отключите *SELinux* коммандой **setenforce 0* и попробуйте зайти. После успешного запуска вы
можете снова включить опцию и настроить её.
### Настраиваем биллинг
Все настройки биллинга находятся в файле *djing/settings.py*. Большинство опций вы можете найти в документации
[Django settings](https://docs.djangoproject.com/en/1.9/ref/settings).
Те опции, которые были добавлены мной в рамках проекта *djing*, описаны ниже в этом разделе документации по установке.
#### djing/settings.py
**USE_TZ** &mdash; Это опция *Django*, но если вы не работаете в разных часовых диапазонах то я не рекомендую включать
эту опцию чтоб небыло путаницы со временем. Это связано с тем что я ещё не тестировал поведение работы со временем при
включённой опции *USE_TZ*.
**ALLOWED_HOSTS** &mdash; Тоже опция *Django*, но важная для безопасности, укажите в списке возможные имена вашего сервера.
Подробнее в документации [Django settings](https://docs.djangoproject.com/en/1.9/ref/settings/#allowed-hosts).
**DEFAULT_PICTURE** &mdash; Это путь к изображению по умолчанию, оно используется когда нужное изображение не найдено.
**PAGINATION_ITEMS_PER_PAGE** &mdash; Количество выводимых элементов списка на странце с таблицей. Например, если поставить 30,
то на странице абонентов на одной странице будет выведено 30 строк абонентов.
**pay_SERV_ID** &mdash; Эта опция, так же как и **pay_SECRET** опции для платёжной системы *AllTime24*, если вы используете любую
другую платёжную систему то можете удалить эти опции.
**DIALING_MEDIA** &mdash; Путь, где биллинг сможет найти файлы записей asterisk чтоб вывести статистику звонков.
Подробнее читайте в описании работы с [АТС](./ats.ms).
**DHCP_TIMEOUT** &mdash; Время аренды настроек DHCP в секундах.
**DEFAULT_SNMP_PASSWORD** &mdash; Пароль snmp по умолчанию для устройств, чтоб при создании устройства он был заполнен в нужном поле.
Если нет такого пароля то оставьте пустым или None.
**TELEPHONE_REGEXP** &mdash; Регулярное выражение для валидации номера телефона.
#### Создание БД
Подразумевается что сервер баз данных у вас уже есть, или вы его можете установить сами.
В конфиге настроить БД можно по инструкции [Django databases](https://docs.djangoproject.com/en/1.9/ref/settings/#databases).
Убедитесть что вы в папке с проектом, комманда **pwd** должна выдать */var/www/djing*.
Чтоб создать бд, как описано в документации [Django admin \& migrate](https://docs.djangoproject.com/en/1.9/ref/django-admin/#migrate),
нужно запустить **./manage.py migrate** чтоб создать структуру БД. Вывод будет примерно таким:
```
$ ./manage.py migrate
Operations to perform:
Apply all migrations: mapapp, contenttypes, dialing_app, django_messages, taskapp, photo_app, accounts_app, devapp, statistics, tariff_app, admin, sessions, chatbot, auth, abonapp
Running migrations:
Rendering model states... DONE
Applying mapapp.0001_initial... OK
Applying devapp.0001_initial... OK
Applying devapp.0002_auto_20160909_1018... OK
Applying devapp.0003_device_map_dot... OK
Applying photo_app.0001_initial... OK
Applying contenttypes.0001_initial... OK
...
Applying taskapp.0012_auto_20170407_0124... OK
Applying taskapp.0013_auto_20170413_1944... OK
Applying taskapp.0014_auto_20170416_1029... OK
Applying taskapp.0015_auto_20170816_1109... OK
```
После этого вам стоит создать супер пользователя чтоб зайти в систему.
```
$ ./manage.py createsuperuser
```
В интерактивном режиме ответьте на вопросы.
```
$ ./manage.py createsuperuser
Username: username
Telephone: +12223334455
Password:
Password (again):
Superuser created successfully.
```
Обратите внимание на то что номер телефона это обязательное поле для заполнения.
Если у вас не выходит указать номер телефона, то проверте чтоб ваш телефон соответствовал регулярному выражению **^\+[7,8,9,3]\d{10,11}$**.
Если регулярное выражение вам не подхожит, то вы можете изенить его в настройках, см. опции в настройках выше.
После изменения настроек они не сразу вступят в силу, нужно перезагрузить код django, для этого перезапустите **uwsgi**:
> \# systemctl restart uwsgi
Теперь произведите тестовый запуск:
> \# ./manage.py runserver 192.168.0.100:8000
Если не подтягивается статика то проверте чтоб опция **DEBUG** в настройках была **True**.
При условии что адрес вашего сервера *192.168.0.100*, вы сможете открыть биллинг по адресу **http://192.168.0.100:8000/**.
Введите логин и пароль супер пользователя которого вы создали по инструкции выше.
Если вы успешно зашли то можно пробовать запускать биллинг в рабочую обстановку.
В настройках смените переменную **DEBUG** на **False** и перезапустите *uwsgi*.
### Настраиваем демоны ### Настраиваем демоны
Если ваша система работает с поддержкой *systemd* то в каталоге *systemd_units* проекта вы найдёте юниты для systemd.
Если ваша система работает с поддержкой [**systemd**](https://www.freedesktop.org/wiki/Software/systemd/) то в каталоге *systemd_units* проекта вы найдёте юниты для systemd.
Скопируйте их в каталог юнитов systemd, у меня это путь */etc/systemd/system*. Скопируйте их в каталог юнитов systemd, у меня это путь */etc/systemd/system*.
__Настоятельно рекомендую заглянуть внутрь этих юнитов__. Проверте пути исполняемых файлов, права и прочее. __Настоятельно рекомендую заглянуть внутрь этих юнитов__. Проверте пути исполняемых файлов, права и прочее.
Для запуска сервиса **djing_rotate.service** вам нужно сначала настроить сбор статистики по [netflow](./netflow.md).
Перед включением юнита *djing_telebot.service* создайте Telegram бота и впишите в файл *djing/settings.py* в переменную *TELEGRAM_BOT_TOKEN* токен вашего бота.
С помощью этого бота вы будете получать различные сообщения из биллинга. Подробнее в инструкции к [модулю оповещений](./bot.md).
А теперь включим и запустим нужные демоны А теперь включим и запустим нужные демоны
```
```shell
# systemctl daemon-reload # systemctl daemon-reload
# systemctl enable djing_queue.service # systemctl enable djing_queue.service
# systemctl start djing_queue.service # systemctl start djing_queue.service
@ -120,5 +288,3 @@ __Настоятельно рекомендую заглянуть внутрь
# systemctl enable djing_telebot.service # systemctl enable djing_telebot.service
# systemctl start djing_telebot.service # systemctl start djing_telebot.service
``` ```
Перед включением юнита *djing_telebot.service* создайте Telegram бота и впишите в файл *djing/settings.py* в переменную *TELEGRAM_BOT_TOKEN* токен вашего бота.
С помощью этого бота вы будете получать различные сообщения из биллинга. Подробнее в инструкции к [модулю оповещений](./docs/bot.md).

9
mydefs.py

@ -10,9 +10,12 @@ from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.db import models from django.db import models
from djing.settings import PAGINATION_ITEMS_PER_PAGE, DEBUG
from django.conf import settings
PAGINATION_ITEMS_PER_PAGE = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
DEBUG = getattr(settings, 'DEBUG', False)
ip_addr_regex = r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' ip_addr_regex = r'^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
@ -83,8 +86,8 @@ class MyGenericIPAddressField(models.GenericIPAddressField):
value = super(MyGenericIPAddressField, self).get_prep_value(value) value = super(MyGenericIPAddressField, self).get_prep_value(value)
return ip2int(value) return ip2int(value)
def to_python(self, addr):
return addr
def to_python(self, value):
return value
def get_internal_type(self): def get_internal_type(self):
return 'PositiveIntegerField' return 'PositiveIntegerField'

7
photo_app/models.py

@ -5,8 +5,9 @@ import hashlib
from django.db import models from django.db import models
from PIL import Image from PIL import Image
from django.conf import settings
from djing.settings import BASE_DIR
BASE_DIR = getattr(settings, 'BASE_DIR', '.')
class Photo(models.Model): class Photo(models.Model):
@ -27,7 +28,7 @@ class Photo(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.image: if not self.image:
return return
super(Photo, self).save()
super(Photo, self).save(*args, **kwargs)
im = Image.open(self.image.path) im = Image.open(self.image.path)
im.thumbnail((759, 759), Image.ANTIALIAS) im.thumbnail((759, 759), Image.ANTIALIAS)
@ -42,7 +43,7 @@ class Photo(models.Model):
im.save(fname) im.save(fname)
os.remove(self.image.path) os.remove(self.image.path)
self.image = "%s.%s" % (hs, ext) self.image = "%s.%s" % (hs, ext)
super(Photo, self).save()
super(Photo, self).save(*args, **kwargs)
# class Meta: # class Meta:
# unique_together = (('image',),) # unique_together = (('image',),)

7
requirements.txt

@ -1,11 +1,16 @@
Django==1.9 Django==1.9
Pillow Pillow
telepot telepot
# for mac address field # for mac address field
netaddr netaddr
# for testing required xmltodict # for testing required xmltodict
xmltodict xmltodict
PyMySQL
# Django recommended mysql client
mysqlclient
easysnmp easysnmp
rq rq
pid pid

3
setup.py

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

3
tariff_app/models.py

@ -19,7 +19,8 @@ class Tariff(models.Model):
ob = [TC for TC in TARIFF_CHOICES if TC[0] == self.calc_type] ob = [TC for TC in TARIFF_CHOICES if TC[0] == self.calc_type]
if len(ob) > 0: if len(ob) > 0:
res_type = ob[0][1] res_type = ob[0][1]
assert issubclass(res_type, TariffBase)
if not issubclass(res_type, TariffBase):
raise TypeError
return res_type return res_type
def calc_deadline(self): def calc_deadline(self):

2
templates/toolbar_page.html

@ -1,3 +1,4 @@
{% if pag.count > 1 %}
{% load dpagination %} {% load dpagination %}
<div class="row"> <div class="row">
<div class="col-sm-4 col-sm-offset-4"> <div class="col-sm-4 col-sm-offset-4">
@ -22,3 +23,4 @@
</ul> </ul>
</div> </div>
</div> </div>
{% endif %}
Loading…
Cancel
Save