Browse Source

Make tests and merge with devel

devel
Dmitry Novikov 8 years ago
parent
commit
2875aabfa3
  1. 5
      README.md
  2. 3
      abonapp/locale/ru/LC_MESSAGES/django.po
  3. 8
      abonapp/templates/abonapp/buy_tariff.html
  4. 3
      abonapp/templates/abonapp/editAbon.html
  5. 4
      abonapp/templates/abonapp/group_list.html
  6. 184
      abonapp/tests.py
  7. 2
      abonapp/urls.py
  8. 52
      abonapp/views.py
  9. 2
      accounts_app/models.py
  10. 4
      agent/core.py
  11. 60
      agent/mod_mikrotik.py
  12. 8
      agent/structs.py
  13. 16
      agent/utils.py
  14. 19
      chatbot/email_bot.py
  15. 5
      chatbot/send_func.py
  16. 9
      devapp/dev_types.py
  17. 13
      devapp/views.py
  18. 2
      dhcp_lever.py
  19. 12
      djing/fields.py
  20. 23
      djing/lib/tln/tln.py
  21. 7
      djing/local_settings.py.template
  22. 14
      djing/settings.py
  23. 2
      docs/dev.md
  24. 28
      docs/dhcp.md
  25. 20
      docs/extra_func.md
  26. 5
      docs/install.md
  27. 2
      docs/map.md
  28. 3
      docs/netflow.md
  29. 14
      docs/user_page.md
  30. 6
      docs/views.md
  31. 1
      group_app/views.py
  32. 2
      msg_app/models.py
  33. 6
      periodic.py
  34. 14
      tariff_app/custom_tariffs.py
  35. 3
      tariff_app/locale/ru/LC_MESSAGES/django.po
  36. 4
      tariff_app/views.py
  37. 2
      taskapp/handle.py
  38. 19
      taskapp/models.py

5
README.md

@ -11,7 +11,10 @@ P.S. Возможно понадобится **Python 3.5** и выше из-з
## Содержание
* [Установка](./docs/install.md)
* [Сервисы и API](./docs/services.md)
* [Разработка расширений](./docs/dev.md)
* [Менеджеры устройств](./docs/dev.md)
* [Сбор информации трафика по netflow](./docs/netflow.md)
* [Работа с представлениями](./docs/views.md)
* [Карта](./docs/map.md)
* [DHCP](./docs/dhcp.md)
* [Страница абонента](./docs/user_page.md)
* [Дополнительный функционал](./docs/extra_func.md)

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

@ -1157,3 +1157,6 @@ msgstr "История задач"
msgid "Charts"
msgstr "Графики"
msgid "Export vCards"
msgstr "Экспорт в vCards"

8
abonapp/templates/abonapp/buy_tariff.html

@ -32,9 +32,9 @@
<select class="form-control" name="tariff" id="id_tariffs">
{% for trf in tariffs %}
{% if trf.pk == selected_tariff %}
<option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d" }}' selected>
<option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d H:i:s" }}' selected>
{% else %}
<option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d" }}'>
<option value="{{ trf.pk }}" data-deadline='{{ trf.calc_deadline|date:"Y-m-d H:i:s" }}'>
{% endif %}
{{ trf.title }}. {{ trf.amount }}{% trans 'currency' %} (Вх:{{ trf.speedIn }}MBit/s. Исх:{{ trf.speedOut }} MBit/s)
</option>
@ -44,11 +44,11 @@
{% if not abon.active_tariff %}
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
<input type="text" class="form-control" name="deadline" id="id_deadline" value="{{ tariffs.0.calc_deadline|date:"Y-m-d" }}">
<input type="text" class="form-control" name="deadline" id="id_deadline" value="{{ tariffs.0.calc_deadline|date:'Y-m-d H:i:s' }}">
<script type="text/javascript">
$(function () {
$('#id_deadline').datetimepicker({
format: 'YYYY-MM-DD'
format: 'YYYY-MM-DD HH:mm:ss'
});
$('#id_tariffs').on('change', function(){
var a = $(this).find('option:selected');

3
abonapp/templates/abonapp/editAbon.html

@ -147,7 +147,8 @@
<div class="col-sm-8 btn-group btn-group-sm">
{% if device %}
<a href="{% url 'devapp:view' group.pk device.pk %}" target="_blank" class="btn btn-sm btn-default" title="{% trans 'Mac Address' %}: {{ device.mac_addr|default:_('Not assigned') }}">
<span class="glyphicon glyphicon-hdd"></span> <span class="hidden-md">{{ device.comment|truncatechars:11 }} {{ device.ip_address|default:'' }}</span>
<span class="glyphicon glyphicon-hdd"></span>
<span class="hidden-md">{{ device.comment|truncatechars:11 }} {{ device.ip_address|default:'' }}</span>
</a>
<a href="{% url 'abonapp:clear_dev' group.pk abon.username %}" class="btn btn-sm btn-danger">
<span class="glyphicon glyphicon-remove-circle"></span> <span class="hidden-xs hidden-lg">{% trans 'Remove clutch' %}</span>

4
abonapp/templates/abonapp/group_list.html

@ -68,6 +68,10 @@
<span class="glyphicon glyphicon-usd"></span> <span class="hidden-xs">{% trans 'Fin report' %}</span>
</a>
{% endif %}
<a href="{% url 'abonapp:vcards' %}" target="_blank" class="btn btn-default">
<span class="glyphicon glyphicon-phone"></span>
<span class="hidden-xs">{% trans 'Export vCards' %}</span>
</a>
</td>
</tr>
</tfoot>

184
abonapp/tests.py

@ -1,3 +1,4 @@
from abc import ABCMeta
from hashlib import md5
from datetime import date
@ -6,12 +7,15 @@ from django.shortcuts import resolve_url
from django.test import TestCase, RequestFactory
from django.conf import settings
from django.utils import timezone
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from xmltodict import parse
from abonapp.models import Abon, AbonStreet, PassportInfo
from abonapp.pay_systems import allpay
from group_app.models import Group
from tariff_app.models import Tariff
from ip_pool.models import NetworkModel
rf = RequestFactory()
@ -24,7 +28,36 @@ def _make_sign(act: int, pay_account: str, serv_id: str, pay_id):
return md.hexdigest()
class AllPayTestCase(TestCase):
class MyBaseTestCase(metaclass=ABCMeta):
def _client_get_check_login(self, url):
"""
Checks if url is protected from unauthorized access
:param url:
:return: authorized response
"""
r = self.client.get(url)
self.assertRedirects(r, "%s?next=%s" % (getattr(settings, 'LOGIN_URL'), url))
self.client.force_login(self.adminuser)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
return r
def setUp(self):
grp = Group.objects.create(title='Grp1')
a1 = Abon.objects.create_user(
telephone='+79781234567',
username='abon',
password='passw1'
)
a1.group = grp
a1.save(update_fields=('group',))
my_admin = UserProfile.objects.create_superuser('+79781234567', 'local_superuser', 'ps')
self.adminuser = my_admin
self.abon = a1
self.group = grp
class AllPayTestCase(MyBaseTestCase, TestCase):
pay_url = '/'
time_format = '%d.%m.%Y %H:%M'
@ -57,18 +90,19 @@ class AllPayTestCase(TestCase):
}
))
r = r.content.decode('utf-8')
self.assertXMLEqual(r, ''.join((
o = ''.join((
"<pay-response>",
"<balance>-13.12</balance>",
"<name>Test Name</name>",
"<account>pay_account1</account>",
"<service_id>%s</service_id>" % service_id,
"<service_id>%s</service_id>" % escape(service_id),
"<min_amount>10.0</min_amount>",
"<max_amount>5000</max_amount>",
"<status_code>21</status_code>",
"<time_stamp>%s</time_stamp>" % current_date,
"<time_stamp>%s</time_stamp>" % escape(current_date),
"</pay-response>"
)))
))
self.assertXMLEqual(r, o)
def user_pay_pay(self):
print('test_user_pay_pay')
@ -88,10 +122,10 @@ class AllPayTestCase(TestCase):
xml = ''.join((
"<pay-response>",
"<pay_id>840ab457-e7d1-4494-8197-9570da035170</pay_id>",
"<service_id>%s</service_id>" % service_id,
"<service_id>%s</service_id>" % escape(service_id),
"<amount>18.21</amount>",
"<status_code>22</status_code>",
"<time_stamp>%s</time_stamp>" % current_date,
"<time_stamp>%s</time_stamp>" % escape(current_date),
"</pay-response>"
))
self.test_pay_time = current_date
@ -113,13 +147,13 @@ class AllPayTestCase(TestCase):
xml = ''.join((
"<pay-response>",
"<status_code>11</status_code>",
"<time_stamp>%s</time_stamp>" % current_date,
"<time_stamp>%s</time_stamp>" % escape(current_date),
"<transaction>",
"<pay_id>840ab457-e7d1-4494-8197-9570da035170</pay_id>",
"<service_id>%s</service_id>" % service_id,
"<service_id>%s</service_id>" % escape(service_id),
"<amount>18.21</amount>",
"<status>111</status>",
"<time_stamp>%s</time_stamp>" % self.test_pay_time,
"<time_stamp>%s</time_stamp>" % escape(self.test_pay_time),
"</transaction>"
"</pay-response>"
))
@ -160,7 +194,7 @@ class AllPayTestCase(TestCase):
self.assertXMLEqual(r, ''.join((
"<pay-response>",
"<status_code>-40</status_code>",
"<time_stamp>%s</time_stamp>" % current_date,
"<time_stamp>%s</time_stamp>" % escape(current_date),
"</pay-response>"
)))
@ -195,7 +229,7 @@ class AllPayTestCase(TestCase):
xml = ''.join((
"<pay-response>",
"<status_code>-10</status_code>",
"<time_stamp>%s</time_stamp>" % current_date,
"<time_stamp>%s</time_stamp>" % escape(current_date),
"</pay-response>"
))
self.assertXMLEqual(r, xml)
@ -209,29 +243,26 @@ class AllPayTestCase(TestCase):
self.non_existing_pay()
class StreetTestCase(TestCase):
class StreetTestCase(MyBaseTestCase, TestCase):
group = None
street = None
def setUp(self):
grp = Group.objects.create(title='Grp1')
super(StreetTestCase, self).setUp()
grp = self.group
self.street = AbonStreet.objects.create(name='test_street', group=grp)
AbonStreet.objects.create(name='test_street1', group=grp)
AbonStreet.objects.create(name='test_street2', group=grp)
AbonStreet.objects.create(name='test_street3', group=grp)
AbonStreet.objects.create(name='test_street4', group=grp)
AbonStreet.objects.create(name='test_street5', group=grp)
self.group = grp
my_admin = UserProfile.objects.create_superuser('+79781234567', 'local_superuser', 'ps')
# self.client.login(username=my_admin.username, password=my_admin.password)
self.adminuser = my_admin
def test_street_make_cyrillic(self):
print('test_make_cyrillic_street')
# title = ''.join(chr(n) for n in range(1072, 1104))
cyrrilic = 'абвгдежзийклмнопрстуфхцчшщъыьэюя'
self.client.force_login(self.adminuser)
url = resolve_url('abonapp:street_add', self.group.pk)
self._client_get_check_login(url)
r = self.client.post(url, {
'name': cyrrilic,
'group': self.group.pk
@ -243,7 +274,7 @@ class StreetTestCase(TestCase):
print('test_edit_steet')
url = resolve_url('abonapp:street_edit', self.group.pk)
streets = AbonStreet.objects.exclude(pk=self.street.pk)
self.client.force_login(self.adminuser)
self._client_get_check_login(url)
r = self.client.post(url, {
'sid': tuple(s.id for s in streets),
'sname': tuple('%s_' % s.name for s in streets)
@ -264,28 +295,17 @@ class StreetTestCase(TestCase):
self.assertEqual(after_count, 0)
class PassportTestCase(TestCase):
class PassportTestCase(MyBaseTestCase, TestCase):
def setUp(self):
grp = Group.objects.create(title='Grp1')
a1 = Abon.objects.create_user(
telephone='+79781234567',
username='pay_account1',
password='passw1'
)
a1.group = grp
a1.save(update_fields=('group',))
super(PassportTestCase, self).setUp()
passport_item = PassportInfo.objects.create(
series='1243',
number='738517',
distributor='Distributor',
date_of_acceptance=date(year=2014, month=9, day=14),
abon=a1
abon=self.abon
)
my_admin = UserProfile.objects.create_superuser('+79781234567', 'local_superuser', 'ps')
self.adminuser = my_admin
self.passport = passport_item
self.abon = a1
self.group = grp
def test_create_update_delete(self):
self.passport_make()
@ -295,7 +315,7 @@ class PassportTestCase(TestCase):
def passport_make(self):
print('passport_make')
url = resolve_url('abonapp:passport_view', self.group.pk, self.abon.username)
self.client.force_login(self.adminuser)
self._client_get_check_login(url)
self.client.post(url, {
'series': '1232',
'number': '123456',
@ -311,7 +331,6 @@ class PassportTestCase(TestCase):
def passport_change(self):
print('passport_change')
url = resolve_url('abonapp:passport_view', self.group.pk, self.abon.username)
self.client.force_login(self.adminuser)
self.client.post(url, {
'series': '9876',
'number': '987654',
@ -327,36 +346,15 @@ class PassportTestCase(TestCase):
def passport_remove_item_with_user(self):
print('passport_remove_item_with_user')
url = resolve_url('abonapp:del_abon', self.group.pk, self.abon.username)
self.client.force_login(self.adminuser)
self.client.post(url)
passport = PassportInfo.objects.filter(abon=self.abon).first()
self.assertIsNone(passport)
class AbonServiceTestCase(TestCase):
def _client_get_check_login(self, url):
"""
Checks if url is protected from unauthorized access
:param url:
:return: authorized response
"""
r = self.client.get(url)
self.assertRedirects(r, "%s?next=%s" % (getattr(settings, 'LOGIN_URL'), url))
self.client.force_login(self.adminuser)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
return r
class AbonServiceTestCase(MyBaseTestCase, TestCase):
def setUp(self):
grp = Group.objects.create(title='Grp1')
a1 = Abon.objects.create_user(
telephone='+79781234567',
username='abon',
password='passw1'
)
a1.group = grp
a1.save(update_fields=('group',))
my_admin = UserProfile.objects.create_superuser('+79781234567', 'local_superuser', 'ps')
super().setUp()
tariff1 = Tariff.objects.create(
title='trf',
descr='descr',
@ -365,7 +363,7 @@ class AbonServiceTestCase(TestCase):
amount=1,
calc_type='Dp'
)
tariff1.groups.add(grp)
tariff1.groups.add(self.group)
tariff1.save()
tariff2 = Tariff.objects.create(
title='trf2',
@ -375,11 +373,8 @@ class AbonServiceTestCase(TestCase):
amount=2,
calc_type='Dp'
)
tariff2.groups.add(grp)
tariff2.groups.add(self.group)
tariff2.save()
self.adminuser = my_admin
self.abon = a1
self.group = grp
self.tariff1 = tariff1
self.tariff2 = tariff2
@ -423,7 +418,7 @@ class AbonServiceTestCase(TestCase):
updated_abon.save(update_fields=('ballance',))
self.client.post(url, data={
'tariff': self.tariff1.pk,
'deadline': self.tariff1.calc_deadline().strftime('%Y-%m-%d')
'deadline': self.tariff1.calc_deadline().strftime('%Y-%m-%d %H:%M:%S')
})
updated_abon = Abon.objects.get(username=self.abon.username)
self.assertEqual(
@ -433,3 +428,62 @@ class AbonServiceTestCase(TestCase):
self.assertEqual(
updated_abon.ballance, 9.0
)
class ClientLeasesTestCase(MyBaseTestCase, TestCase):
def setUp(self):
super(ClientLeasesTestCase, self).setUp()
netw = NetworkModel.objects.create(
network='192.168.0.0/24',
kind='inet',
description='Descr',
ip_start='192.168.0.3',
ip_end='192.168.0.6'
)
netw.groups.add(self.group.pk)
netw.save()
self.network = netw
def test_add_static_ipv4_lease(self):
print('test_add_static_ipv4_lease')
url = resolve_url('abonapp:lease_add', gid=self.group.pk, uname=self.abon.username)
self._client_get_check_login(url)
# Checks if lease not in allowed range
r = self.client.post(url, data={
'ip_addr': '192.168.0.255',
'is_dynamic': False,
'possible_networks': self.network.pk
})
self.assertFormError(r, form='form', field='ip_addr', errors=_('Ip that you have passed is greater than allowed network range'))
# Not valid ipv4 address
r = self.client.post(url, data={
'ip_addr': '192.168.3.213123',
'is_dynamic': False,
'possible_networks': self.network.pk
})
self.assertFormError(r, form='form', field='ip_addr', errors=_('Enter a valid IPv4 or IPv6 address.'))
# different subnet
r = self.client.post(url, data={
'ip_addr': '192.168.4.2',
'is_dynamic': False,
'possible_networks': self.network.pk
})
self.assertFormError(r, form='form', field='ip_addr', errors=_('Ip that you typed is not in subnet that you have selected'))
# another subnet
netw = NetworkModel.objects.create(
network='192.168.1.0/24',
kind='inet',
description='Descr',
ip_start='192.168.1.3',
ip_end='192.168.1.6'
)
r = self.client.post(url, data={
'ip_addr': '192.168.0.9',
'is_dynamic': False,
'possible_networks': netw.pk
})
self.assertFormError(r, form='form', field='ip_addr', errors=_('Ip that you typed is not in subnet that you have selected'))

2
abonapp/urls.py

@ -55,6 +55,8 @@ urlpatterns = [
url(r'^pay$', views.terminal_pay, name='terminal_pay'),
url(r'^debtors$', views.DebtorsListView.as_view(), name='debtors'),
url(r'^ping$', views.abon_ping, name='ping'),
url(r'^contacts/vcards/$', views.vcards, name='vcards'),
# Api's
url(r'^api/abons$', views.abons),
url(r'^api/abon_filter$', views.search_abon),

52
abonapp/views.py

@ -1,10 +1,9 @@
from typing import Dict, Optional
from datetime import datetime, date, timedelta
from datetime import datetime, date
from django.contrib.gis.shortcuts import render_to_text
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, ProgrammingError, transaction
from django.db.models import Count, Q
from django.http.request import QueryDict
from django.shortcuts import render, redirect, get_object_or_404, resolve_url
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
@ -420,8 +419,7 @@ def pick_tariff(request, gid, uname):
if deadline == '' or deadline is None:
abon.pick_tariff(trf, request.user, comment=log_comment)
else:
deadline = datetime.strptime(deadline, '%Y-%m-%d')
deadline += timedelta(hours=23, minutes=59, seconds=59)
deadline = datetime.strptime(deadline, '%Y-%m-%d %H:%M:%S')
abon.pick_tariff(trf, request.user, deadline=deadline, comment=log_comment)
abon.sync_with_nas(created=False)
messages.success(request, _('Tariff has been picked'))
@ -705,7 +703,49 @@ def abon_ping(request):
}
@method_decorator((login_required, lib.decorators.only_admins,), name='dispatch')
@login_required
def vcards(r):
abons = models.Abon.objects.exclude(group=None).select_related('group', 'street').only(
'username', 'fio', 'group__title', 'telephone',
'street__name', 'house'
)
additional_tels = models.AdditionalTelephone.objects.select_related('abon', 'abon__group', 'abon__street')
response = HttpResponse(content_type='text/x-vcard')
response['Content-Disposition'] = 'attachment; filename="contacts.vcard"'
tmpl = ("BEGIN:VCARD\r\n"
"VERSION:4.0\r\n"
"FN:%(uname)s. %(group_name)s, %(street)s %(house)s\r\n"
"IMPP:sip:%(abon_telephone)s@dial.lo\r\n"
"END:VCARD\r\n")
def _make_vcard():
for ab in abons.iterator():
tel = ab.telephone
if tel:
yield tmpl % {
'uname': ab.get_full_name(),
'group_name': ab.group.title,
'street': ab.street.name if ab.street else '',
'house': ab.house,
'abon_telephone': tel
}
if not additional_tels.exists():
return
for add_tel in additional_tels.iterator():
abon = add_tel.abon
yield tmpl % {
'uname': "%s (%s)" % (add_tel.owner_name, abon.get_full_name()),
'group_name': abon.group.title,
'abon_telephone': add_tel.telephone,
'street': abon.street.name if abon.street else '',
'house': abon.house
}
response.content = _make_vcard()
return response
@method_decorator((login_required, lib.decorators.only_admins), name='dispatch')
class DialsListView(OrderedFilteredList):
context_object_name = 'logs'
template_name = 'abonapp/dial_log.html'
@ -1183,7 +1223,7 @@ class DhcpLever(SecureApiView):
def on_dhcp_event(data: Dict) -> Optional[str]:
"""
data = {
'client_ip': ip2int('127.0.0.1'),
'client_ip': ip_address('127.0.0.1'),
'client_mac': 'aa:bb:cc:dd:ee:ff',
'switch_mac': 'aa:bb:cc:dd:ee:ff',
'switch_port': 3,

2
accounts_app/models.py

@ -95,7 +95,7 @@ class UserProfileManager(MyUserManager):
class UserProfile(BaseAccount):
avatar = models.ImageField(_('Avatar'), upload_to=os.path.join('user', 'avatar'), null=True, default=None)
email = models.EmailField(default='admin@example.ru')
email = models.EmailField(default='')
responsibility_groups = models.ManyToManyField(Group, blank=True, verbose_name=_('Responsibility groups'))
objects = UserProfileManager()

4
agent/core.py

@ -97,8 +97,10 @@ class BaseTransmitter(ABC):
:return: Tuple of 2 lists that contain list to add users and list to remove users
"""
users_struct_list = (ab.build_agent_struct() for ab in users_from_db if ab.is_access())
users_struct_set = set([ab for ab in users_struct_list if ab is not None and ab.tariff is not None])
users_struct_set = set(ab for ab in users_struct_list if ab is not None and ab.tariff is not None)
users_from_nas = set(self.read_users())
if len(users_from_nas) < 1:
print('WARNING: Not have users from NAS')
list_for_del = (users_struct_set ^ users_from_nas) - users_struct_set
list_for_add = users_struct_set - users_from_nas
return list_for_add, list_for_del

60
agent/mod_mikrotik.py

@ -3,13 +3,14 @@ import socket
import binascii
from abc import ABCMeta
from hashlib import md5
from ipaddress import ip_network
from ipaddress import ip_network, _BaseAddress
from typing import Iterable, Optional, Tuple, Generator, Dict
from django.conf import settings
from djing.lib.decorators import LazyInitMetaclass
from .structs import TariffStruct, AbonStruct, IpStruct, VectorAbon, VectorTariff
from . import settings as local_settings
from django.conf import settings
from djing import ping
from agent.core import BaseTransmitter, NasNetworkError, NasFailedResult
@ -231,21 +232,28 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
res = float(re.sub(r'[a-zA-Z]', '', text_speed)) / 1000 ** 2
return res
dat = info.get('!re')
speeds = dat.get('=max-limit').split('/')
speed_out, speed_in = info['=max-limit'].split('/')
t = TariffStruct(
speed_in=parse_speed(speeds[1]),
speed_out=parse_speed(speeds[0])
speed_in=parse_speed(speed_in),
speed_out=parse_speed(speed_out)
)
try:
target = info.get('=target')
if target is None:
target = info.get('=target-addresses')
name = info.get('=name')
disabled = info.get('=disabled')
if disabled is not None:
disabled = True if disabled == 'true' else False
if target is not None and name is not None:
target_ip, target_net = target.split('/')
a = AbonStruct(
uid=int(dat['=name'][3:]),
# FIXME: тут в разных микротиках или =target-addresses или =target
ips=(int(ip_network(ip).network_address) for ip in dat['=target'].split(',')),
uid=int(name[3:]),
ip=target_ip,
tariff=t,
is_access=False if dat['=disabled'] == 'false' else True
is_active=disabled or False
)
a.queue_id = dat['=.id']
a.queue_id = info.get('=.id')
return a
except ValueError:
pass
@ -348,8 +356,8 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
'=numbers=%s' % ','.join(ip_firewall_ids)
))
def find_ip(self, ip: IpStruct, list_name: str):
if not isinstance(ip, IpStruct):
def find_ip(self, ip, list_name: str):
if not issubclass(ip.__class__, _BaseAddress):
raise TypeError
r = self._exec_cmd((
'/ip/firewall/address-list/print', 'where',
@ -407,7 +415,7 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
# queue is instance of AbonStruct
queue = self.find_queue('uid%d' % user.uid)
for ip in user.ips:
if not isinstance(ip, IpStruct):
if not issubclass(ip.__class__, _BaseAddress):
raise TypeError
nas_ip = self.find_ip(ip, LIST_USERS_ALLOWED)
if user.is_access:
@ -469,14 +477,28 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
pass
def read_users(self) -> VectorAbon:
class ip_mkid_struct(object):
__slots__ = ('ip', 'mkid')
def __init__(self, ip, mkid):
self.ip = ip
self.mkid = mkid
def __eq__(self, other):
if isinstance(other, ip_mkid_struct):
return self.ip == other.ip
return self.ip == str(other)
def __hash__(self):
return hash(self.ip)
# shapes is ShapeItem
allowed_ips = tuple(self.read_ips_iter(LIST_USERS_ALLOWED))
queues = tuple(q for q in self.read_queue_iter() if set(q.ips).issubset(allowed_ips))
# TODO: Make clean old ip addresses in other place
#ips_from_queues = set((q.ip, q) for q in queues)
all_ips = set(ip_mkid_struct(ip, mkid) for ip, mkid in self.read_ips_iter(LIST_USERS_ALLOWED))
queues = (q for q in self.read_queue_iter() if str(q.ip) in all_ips)
# ips_from_queues = set(str(q.ip) for q in queues)
# delete ip addresses that are in firewall/address-list and there are no corresponding in queues
#diff = tuple(allowed_ips - ips_from_queues)
#diff = tuple(all_ips - ips_from_queues)
#if len(diff) > 0:
# self.remove_ip_range(diff)
return queues

8
agent/structs.py

@ -44,9 +44,9 @@ class TariffStruct(BaseStruct):
self.speedIn = speed_in or 0
self.speedOut = speed_out or 0
# Да, если все значения нулевые
# Yes, if all variables is zeroed
def is_empty(self):
return self.tid == 0 and self.speedIn == 0.001 and self.speedOut == 0.001
return self.tid == 0 and self.speedIn == 0 and self.speedOut == 0
def __eq__(self, other):
# не сравниваем id, т.к. тарифы с одинаковыми скоростями для NAS одинаковы
@ -62,7 +62,7 @@ class TariffStruct(BaseStruct):
return hash(str(self.speedIn) + str(self.speedOut))
# Абонент из базы
# Abon from database
class AbonStruct(BaseStruct):
__slots__ = ('uid', 'ips', 'tariff', 'is_access', 'queue_id')
@ -90,7 +90,7 @@ class AbonStruct(BaseStruct):
return hash(hash(self.ips) + hash(self.tariff)) if self.tariff is not None else 0
# Правило шейпинга в фаере, или ещё можно сказать услуга абонента на NAS
# Shape rule from NAS(Network Access Server)
class ShapeItem(BaseStruct):
__slots__ = ('abon', 'sid')

16
agent/utils.py

@ -1,16 +0,0 @@
import socket
import struct
def ip2int(addr):
try:
return struct.unpack("!I", socket.inet_aton(addr))[0]
except:
return 0
def int2ip(addr):
try:
return socket.inet_ntoa(struct.pack("!I", addr))
except:
return ''

19
chatbot/email_bot.py

@ -0,0 +1,19 @@
from smtplib import SMTPException
from django.core.mail import send_mail
from django.conf import settings
from chatbot.models import ChatException, MessageQueue
def send_notify(msg_text, account, tag='none'):
try:
MessageQueue.objects.push(msg=msg_text, user=account, tag=tag)
target_email = account.email
send_mail(
subject=getattr(settings, 'COMPANY_NAME', 'Djing notify'),
message=msg_text,
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL'),
recipient_list=(target_email,)
)
except SMTPException as e:
raise ChatException('SMTPException: %s' % e)

5
chatbot/send_func.py

@ -0,0 +1,5 @@
# send via email
from .email_bot import send_notify
# for Telegram
# from chatbot.telebot import send_notify

9
devapp/dev_types.py

@ -499,12 +499,13 @@ class ZteOnuDevice(OnuDevice):
raise DeviceConfigurationError('For ZTE configuration needed "telnet" section in extra_data')
login = telnet.get('login')
password = telnet.get('password')
if login is None or password is None:
raise DeviceConfigurationError('For ZTE configuration needed login and'
' password for telnet access in extra_data')
prompt = telnet.get('prompt')
if login is None or password is None or prompt is None:
raise DeviceConfigurationError('For ZTE configuration needed login, password and'
' prompt for telnet access in extra_data')
stack_num, rack_num, fiber_num, new_onu_port_num = register_onu_ZTE_F660(
olt_ip=ip, onu_sn=sn, login_passwd=(login.encode(), password.encode()),
onu_mac=mac
onu_mac=mac, prompt_title=prompt.encode(), vlan_id=132
)
bin_snmp_fiber_number = "10000{0:08b}{1:08b}00000000".format(rack_num, fiber_num)
snmp_fiber_num = int(bin_snmp_fiber_number, base=2)

13
devapp/views.py

@ -1,4 +1,5 @@
import re
from ipaddress import ip_address
from django.contrib.auth.decorators import login_required
from django.contrib.gis.shortcuts import render_to_text
from django.core.exceptions import PermissionDenied
@ -22,7 +23,7 @@ from accounts_app.models import UserProfile
from django.conf import settings
from guardian.decorators import permission_required_or_403 as permission_required
from guardian.shortcuts import get_objects_for_user
from chatbot.telebot import send_notify
from chatbot.send_func import send_notify
from chatbot.models import ChatException
from jsonview.decorators import json_view
from djing import global_base_views, MAC_ADDR_REGEX, ping, get_object_or_None
@ -519,9 +520,13 @@ def search_dev(request):
if word is None or word == '':
results = [{'id': 0, 'text': ''}]
else:
results = Device.objects.filter(
Q(comment__icontains=word) | Q(ip_address=word)
).only('pk', 'ip_address', 'comment')[:16]
qs = Q(comment__icontains=word)
try:
ip = ip_address(word)
qs |= Q(ip_address=str(ip))
except ValueError:
pass
results = Device.objects.filter(qs).only('pk', 'ip_address', 'comment')[:16]
results = [{
'id': device.pk,
'text': "%s: %s" % (device.ip_address or '', device.comment)

2
dhcp_lever.py

@ -16,7 +16,7 @@ def die(text):
'''
obj = {
'client_ip': ip2int('127.0.0.1'),
'client_ip': ip_address('127.0.0.1'),
'client_mac': 'aa:bb:cc:dd:ee:ff',
'switch_mac': 'aa:bb:cc:dd:ee:ff',
'switch_port': 3,

12
djing/fields.py

@ -1,11 +1,11 @@
#
# I got it on https://github.com/django-macaddress/django-macaddress
#
from ipaddress import ip_address
from netaddr import EUI, AddrFormatError
from django.core.exceptions import ValidationError
from django.db import models
from netaddr import EUI, AddrFormatError
from djing.lib import ip2int, int2ip
from .formfields import MACAddressField as MACAddressFormField
from . import default_dialect
import warnings
@ -121,7 +121,8 @@ class MyGenericIPAddressField(models.GenericIPAddressField):
def get_prep_value(self, value):
# strIp to Int
value = super(MyGenericIPAddressField, self).get_prep_value(value)
return ip2int(value)
if value:
return int(ip_address(value))
def to_python(self, value):
return value
@ -131,7 +132,8 @@ class MyGenericIPAddressField(models.GenericIPAddressField):
@staticmethod
def from_db_value(value, expression, connection, context):
return int2ip(value) if value != 0 else None
if value:
return str(ip_address(value))
def int_ip(self):
return ip2int(self)
return int(ip_address(self))

23
djing/lib/tln/tln.py

@ -40,12 +40,11 @@ ONU_SN_REGEX = b'^ZTEG[A-F\d]{8}$'
class TelnetApi(Telnet):
def __init__(self, *args, **kwargs):
def __init__(self, prompt_string: bytes, *args, **kwargs):
timeout = kwargs.get('timeout')
if timeout:
self._timeout = timeout
self._prompt_string = b'ZTE-C320-PKP#'
self.config_level = []
self._prompt_string = prompt_string or b'ZTE#'
super().__init__(*args, **kwargs)
def write(self, buffer: bytes) -> None:
@ -106,8 +105,10 @@ def parse_onu_name(onu_name: bytes, name_regexp=re.compile(b'[/:_]')) -> Dict[st
class OltZTERegister(TelnetApi):
def __init__(self, screen_size: Tuple[int, int], *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, screen_size: Tuple[int, int], prompt_title: bytes, *args, **kwargs):
super().__init__(prompt_string=prompt_title, *args, **kwargs)
self.prompt_title = prompt_title
self.set_prompt_string(b'%s#' % prompt_title)
self.resize_screen(*screen_size)
def enter(self, username: bytes, passw: bytes) -> None:
@ -153,7 +154,7 @@ class OltZTERegister(TelnetApi):
return last_onu
def enter_to_config_mode(self) -> bool:
prompt = b'ZTE-C320-PKP(config)#'
prompt = b'%s(config)#' % self.prompt_title
self.set_prompt_string(prompt)
res = tuple(self.command_to(b'config terminal'))
if res[1].startswith(b'Enter configuration commands'):
@ -162,7 +163,7 @@ class OltZTERegister(TelnetApi):
return False
def go_to_olt_interface(self, stack_num: int, rack_num: int, fiber_num: int) -> Tuple:
self.set_prompt_string(b'ZTE-C320-PKP(config-if)#')
self.set_prompt_string(b'%s(config-if)#' % self.prompt_title)
return tuple(self.command_to(b'interface gpon-olt_%d/%d/%d' % (
stack_num,
rack_num,
@ -170,7 +171,7 @@ class OltZTERegister(TelnetApi):
)))
def go_to_onu_interface(self, stack_num: int, rack_num: int, fiber_num: int, onu_port_num: int) -> Tuple:
self.set_prompt_string(b'ZTE-C320-PKP(config-if)#')
self.set_prompt_string(b'%s(config-if)#' % self.prompt_title)
return tuple(self.command_to(b'interface gpon-onu_%d/%d/%d:%d' % (
stack_num,
rack_num,
@ -204,7 +205,7 @@ class OltZTERegister(TelnetApi):
@process_lock
def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes, bytes], onu_mac: bytes) -> Tuple:
def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes, bytes], onu_mac: bytes, prompt_title: bytes, vlan_id: int) -> Tuple:
onu_type = b'ZTE-F660'
line_profile = b'ZTE-F660-LINE'
remote_profile = b'ZTE-F660-ROUTER'
@ -215,7 +216,7 @@ def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes,
if not re.match(ONU_SN_REGEX, onu_sn):
raise ValidationError
tn = OltZTERegister(host=olt_ip, timeout=2, screen_size=(120, 128))
tn = OltZTERegister(host=olt_ip, timeout=2, screen_size=(120, 128), prompt_title=prompt_title)
tn.enter(*login_passwd)
unregistered_onu = tn.get_unregistered_onu(onu_sn)
@ -254,7 +255,7 @@ def register_onu_ZTE_F660(olt_ip: str, onu_sn: bytes, login_passwd: Tuple[bytes,
r = tn.go_to_onu_interface(stack_num, rack_num, fiber_num, new_onu_port_num)
print(r)
tn.apply_conf_to_onu(onu_mac, 145)
tn.apply_conf_to_onu(onu_mac, vlan_id)
sleep(1)
return stack_num, rack_num, fiber_num, new_onu_port_num

7
djing/local_settings.py.template

@ -62,3 +62,10 @@ API_AUTH_SUBNET = '127.0.0.0/8'
# Company name
COMPANY_NAME = 'Your company name'
# Email config
EMAIL_HOST_USER = 'YOUR-EMAIL@mailserver.com'
EMAIL_HOST = 'smtp.mailserver.com'
EMAIL_PORT = 587
EMAIL_HOST_PASSWORD = 'password'
EMAIL_USE_TLS = True

14
djing/settings.py

@ -131,11 +131,11 @@ SESSION_COOKIE_HTTPONLY = True
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'ru'
LANGUAGE_CODE = 'en'
LANGUAGES = (
('ru', _('Russian')),
#('en', _('English'))
('en', _('English'))
)
PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))
@ -214,6 +214,16 @@ BOOTSTRAP3 = {
'horizontal_field_class': 'col-md-9',
}
# Email config
EMAIL_HOST_USER = local_settings.EMAIL_HOST_USER
EMAIL_HOST = local_settings.EMAIL_HOST
EMAIL_PORT = local_settings.EMAIL_PORT
EMAIL_HOST_PASSWORD = local_settings.EMAIL_HOST_PASSWORD
EMAIL_USE_TLS = getattr(local_settings, 'EMAIL_USE_TLS', True)
SERVER_EMAIL = EMAIL_HOST_USER
# Inactive ip lease time in seconds.
# If lease time more than time of create, and lease is inactive
# then delete it. Used in ip_pool app.

2
docs/dev.md

@ -282,7 +282,7 @@ def add_tariff_range(self, tariff_list):
Для того чтоб оправить важное сообщение работнику через все возможные настроенные системы(смс, телеграм, браузер) мы можем
воспользоваться одной процедурой из модуля **chatbot**.
```python
from chatbot.telebot import send_notify
from chatbot.send_func import send_notify
send_notify(msg_text='Text message',account=employee_profile, tag='apptag')
```

28
docs/dhcp.md

@ -0,0 +1,28 @@
## ISC-DHCP Сервер, взаимодействие с биллингом.
Вобщих чертах взаимодействие происходит с помощью скрипта **dhcp_lever.py**
в корне проекта. Запущенный DHCP сервер, при возникновении событий запускает
этот сценарий , а тот говорит биллингу подробнее что произошло.
При событии *expiry* или *release* биллингу нужно освободить ip, а при *commit*
нужно назначить динамическую аренду ip для учётной записи абонента в биллинге.
Сам скрипт не выполняет все эти действия, он просто отправляет полученные от dhcp
сервера параметры на url адрес для обработки dhcp. View распологается в **abonapp.views.DhcpLever**.
### Выделение аренды ip
Как происходит выделение аренды ip, от события в dhcp сервере и до появления интернета у
абонента.
Когда в dhcp сервере происходит событие *commit* то из **abonapp.views.DhcpLever** вызывается
функция **agent.commands.dhcp.dhcp_commit**, с помощью DHCP OPTION.82 получаем mac адрес управляемого
свича и порт через который пришёл запрос. Каждое такое устройство должно быть зарегистрировано в биллинге.
Далее ищем в базе абонента, или абонентов к которому привязано устройство с переданным mac адресом.
Проверяем может-ли данный тип устройства содержать несколько подключённых абонентов(напрмер PON ONU, в основном,
содержит одного абонента). Проверка происходит по свойству **is_use_device_port** из менеджера устройства,
которое открыто для кастомизации, подробнее в [Менеджер устройства](./docs/dev.md).
А далее, если может быть несколько абонентов, то фильтруем вывод ещё по порту свича.
Получется что на управляемом свиче мы авторизуем абонентов при помощи dhcp option.82 по маку свича и порту абонента.
Если наше устройство PON ONU(ONT) то авторизуем только по mac адресу оптического юнита(onu).
После добавления абоненту аренды динамического ip, он(абонент) синхронизуется с nas сервером и открывается доступ
к интернету в соответствии с тарифом абонента.

20
docs/extra_func.md

@ -0,0 +1,20 @@
## Дополнительный фунционал
В процессе реализации проекта понадобился функционал, который отсутствует в базовой поставке **Django**.
Его совсем не много, но без внимания оставить нельзя.
Все вспомогательные модули можно найти в пакете **djing.lib**.
### tln
Это модуль работы по *telnet*
### messaging
Этот модуль помогает работать с форматами СМС сообщений.
### init
Содержит всякие мелкие примочки, код прост и с комментариями, зайдите посмотрите.
## auth_decorators
Бэкенд авторизации
## decorators
Дополнительные декораторы.

5
docs/install.md

@ -12,10 +12,10 @@
Затем установим зависимости
```
# dnf -y install python3 python3-devel python3-pip python3-pillow mariadb mariadb-devel uwsgi nginx uwsgi-plugin-python3 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 net-snmp net-snmp-libs net-snmp-utils net-snmp-devel net-snmp-python git redhat-rpm-config curl-devel
```
Лучше чтоб версия python по умолчанию была третья:
Необходимо чтоб версия python по умолчанию была третья:
```
# ln -sf python3 /usr/bin/python
```
@ -27,6 +27,7 @@
# cd /var/www
# pip3 install --upgrade pip
# git clone https://github.com/nerosketch/djing.git
# export PYCURL_SSL_LIBRARY=openssl
# pip3 install -r djing/requirements.txt
```

2
docs/map.md

@ -24,4 +24,4 @@
### Другое
Статусы устройств на точках с одним устройством обновляются автоматически 2 раза в минуту.
Это означает что когда вы изучаете карту и какое-либо устройство пропало из сети то вы
увидите это сразу без перезагрузки карты.
увидите это в течение 2х минут без перезагрузки карты.

3
docs/netflow.md

@ -27,5 +27,6 @@ rm -rf djing_flow_git
вы найдёте этот самый файл дампа трафика. И тут уже можно посмотреть как работает утилита **djing_flow**:
> \$ ./djing_flow < /tmp/djing_flow_dump.tmp
На выходе вы получите запрос для mysql. Можно перенаправить его по конвееру в mysql, реализацию вы можете увидеть в файле
На выходе вы получите запрос для mysql. Можно перенаправить его по конвееру в mysql, рабочий пример
перенаправления этогй утилиты вы можете увидеть в файле
*agent/netflow/netflow_handler.sh*.

14
docs/user_page.md

@ -0,0 +1,14 @@
## Особенности страницы абонента.
Находится она в разделе **Абоненты** внутри группы. На этой странице вы увидите несколько логических блоков,
из которых самые важные, пожалуй, 2. Первый это **Изменение абонента** а второй **Выберите устройство**.
На первом блоке можно редактировать базовую информацию абонента. Если снять галку *Активен* то абонент перестанет
получать услуги даже при подключённой услуге.
Блок с устройством содержит то самое устройство, к которому подключён абонент. Если это устройство не будет назначено
то биллинг не сможет авторизовать абонента по dhcp option.82. Галочка **Динамические настройки по dhcp** означает что
учётная запись абонента сможет получать динамический ip. Это означает что если галка не будет выставлена, то сколько бы
запросов не приходило с этого устройства абонент не изменить свой ip, это полезно когда абонент работает со статическим
ip.
Вверху есть вкладки. с соответствующим названию функционалом. Например на вкладке **Тарифы** вы можете назначить
абоненту услугу или добавить периодический платёж, который абонент увидит в своём личном кабинете.

6
docs/views.md

@ -22,8 +22,8 @@ class PaysListView(ListView):
```
Тогда в шаблоне с bootstrap вы можете увидеть примерно такую пагинацию которую
вы конечно же можете изменить на свою.
Тогда в шаблоне с bootstrap вы можете подключить шаблон пагинации *templates/pagination.html* и
увидеть примерно такую пагинацию которую вы конечно же можете изменить на свою.
![paginator](./img/pagination.png).
@ -53,4 +53,4 @@ class PaysListView(ListView, OrderingMixin):
pass
```
Примесь *OrderingMixin* добавляет в контекст переменные *order_by* и *dir* для использования в шалоне.
Примесь *OrderingMixin* добавляет в контекст переменные *order_by* и *dir* для использования в шаблоне.

1
group_app/views.py

@ -69,7 +69,6 @@ class DeleteGroupView(DeleteView):
group_with_subscribers = models.Group.objects.annotate(
subscribers_count=Count('abon')
).filter(subscribers_count__gt=0, pk=group_id).first()
print('group_with_subscribers:', group_with_subscribers)
if group_with_subscribers is not None:
messages.error(request, _('Group is contain subscribers. Remove them before delete group'))
return HttpResponseRedirect(self.success_url)

2
msg_app/models.py

@ -1,7 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts_app.models import UserProfile
from chatbot.telebot import send_notify
from chatbot.send_func import send_notify
from chatbot.models import ChatException

6
periodic.py

@ -40,9 +40,9 @@ def main():
try:
tm = Transmitter()
users = Abon.objects.filter(is_active=True).exclude(current_tariff=None)
tm.sync_nas(users)
except NasNetworkError as e:
print('NetworkTrouble:', e)
tm.sync_nas(users.iterator())
except NasNetworkError as er:
print('NetworkTrouble:', er)
# manage periodic pays
ppays = PeriodicPayForId.objects.filter(next_pay__lt=now) \

14
tariff_app/custom_tariffs.py

@ -67,11 +67,23 @@ class TariffCp(TariffDp):
return long_long_time
# Daily service
class TariffDaily(TariffDp):
description = _('IS Daily service')
def calc_deadline(self):
nw = timezone.now()
# next day in the same time
one_day = timedelta(days=1)
return nw + one_day
# Первый - всегда по умолчанию
TARIFF_CHOICES = (
('Df', TariffDefault),
('Dp', TariffDp),
('Cp', TariffCp)
('Cp', TariffCp),
('Dl', TariffDaily)
)

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

@ -212,3 +212,6 @@ msgstr "Периодический платёж изменён"
msgid "Are you sure you want to delete tariff?"
msgstr "Вы уверены что хотите удалить тариф?"
msgid "IS Daily service"
msgstr "Услуга на сутки"

4
tariff_app/views.py

@ -40,9 +40,9 @@ def edit_tarif(request, tarif_id=0):
if request.method == 'POST':
frm = forms.TariffForm(request.POST, instance=tarif)
if frm.is_valid():
frm.save()
new_service = frm.save()
messages.success(request, _('Service has been saved'))
return redirect('tarifs:edit', tarif_id=tarif_id)
return redirect('tarifs:edit', tarif_id=new_service.pk)
else:
messages.warning(request, _('Some fields were filled incorrect, please try again'))
else:

2
taskapp/handle.py

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from django.utils.translation import gettext as _
from chatbot.telebot import send_notify
from chatbot.send_func import send_notify
from chatbot.models import ChatException
from djing.lib import MultipleException

19
taskapp/models.py

@ -6,7 +6,7 @@ from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext as _
from abonapp.models import Abon
# from .handle import handle as task_handle
from .handle import handle as task_handle
TASK_PRIORITIES = (
('A', _('Higher')),
@ -100,15 +100,14 @@ class Task(models.Model):
self.save(update_fields=('state',))
def send_notification(self):
pass
#if self.abon:
# group = self.abon.group
#else:
# group = ''
#task_handle(
# self, self.author,
# self.recipients.all(), group
#)
if self.abon:
group = self.abon.group
else:
group = ''
task_handle(
self, self.author,
self.recipients.all(), group
)
def get_attachment_fname(self):
return os.path.basename(self.attachment.name)

Loading…
Cancel
Save