Browse Source

Merge branch 'fix_shaper' into devel

devel
Dmitry Novikov 7 years ago
parent
commit
93f8fdde3b
  1. 30
      abonapp/forms.py
  2. 20
      abonapp/locale/ru/LC_MESSAGES/django.po
  3. 51
      abonapp/migrations/0006_change_ip.py
  4. 61
      abonapp/models.py
  5. 83
      abonapp/templates/abonapp/editAbon.html
  6. 24
      abonapp/templates/abonapp/modal_add_lease.html
  7. 27
      abonapp/templates/abonapp/modal_ip_form.html
  8. 7
      abonapp/urls.py
  9. 191
      abonapp/views.py
  10. 6
      accounts_app/templatetags/acc_tags.py
  11. 37
      agent/commands/dhcp.py
  12. 2
      dhcp_lever.py
  13. 8
      djing/lib/auth_backends.py
  14. 9
      djing/lib/mixins.py
  15. 1
      ip_pool/admin.py
  16. 38
      ip_pool/forms.py
  17. 43
      ip_pool/migrations/0003_auto_20181015_1430.py
  18. 61
      ip_pool/models.py
  19. 44
      ip_pool/templates/ip_pool/ip_leases_list.html
  20. 4
      ip_pool/templates/ip_pool/net_edit.html
  21. 6
      ip_pool/templates/ip_pool/network_list.html
  22. 1
      ip_pool/urls.py
  23. 17
      ip_pool/views.py
  24. 2
      nas_app/nas_managers/__init__.py
  25. 80
      nas_app/nas_managers/core.py
  26. 388
      nas_app/nas_managers/mod_mikrotik.py
  27. 110
      nas_app/nas_managers/structs.py
  28. 4
      nas_app/views.py
  29. 10
      periodic.py
  30. 2
      searchapp/views.py
  31. 12
      systemd_units/djing_backup.service
  32. 11
      systemd_units/djing_backup.timer
  33. 8
      systemd_units/do_backup.sh

30
abonapp/forms.py

@ -4,6 +4,8 @@ from django.contrib.auth.hashers import make_password
from random import choice
from string import digits, ascii_lowercase
from djing.lib import LogicError
from ip_pool.models import NetworkModel
from nas_app.models import NASModel
from . import models
from django.conf import settings
@ -166,5 +168,31 @@ class MarkersForm(forms.ModelForm):
class AmountMoneyForm(forms.Form):
amount = forms.FloatField(max_value=50000, label=_('Amount of money'))
amount = forms.FloatField(max_value=5000, label=_('Amount of money'))
comment = forms.CharField(max_length=128, label=_('Comment'), required=False)
class AddIpForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = getattr(self, 'instance')
if instance:
if instance.group:
self.fields['networks'].queryset = NetworkModel.objects.filter(groups=instance.group)
if not self.initial['ip_address']:
if instance:
net = NetworkModel.objects.filter(groups=instance.group).first()
if net is not None:
ips = (ip.ip_address for ip in
models.Abon.objects.filter(group=instance.group).order_by('ip_address').only(
'ip_address').iterator())
free_ip = net.get_free_ip(ips)
self.initial['ip_address'] = free_ip
else:
raise LogicError(_('Subnet has not attached to current group'))
networks = forms.ModelChoiceField(label=_('Networks'), queryset=NetworkModel.objects.none(), empty_label=None)
class Meta:
model = models.Abon
fields = 'ip_address',

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

@ -116,8 +116,8 @@ msgid "User group"
msgstr "Группа"
#: models.py:96 templates/abonapp/editAbon.html:180
msgid "Ip addresses"
msgstr "IP Адреса"
msgid "Ip address"
msgstr "IP Адрес"
#: models.py:103
msgid "Network access server"
@ -202,11 +202,6 @@ msgstr "Не хватает денег на счету"
msgid "Buy service default log"
msgstr "Покупка тарифного плана через админку"
#: models.py:228
#, python-format
msgid "Account \"%(username)s\" not have any active leases"
msgstr "Учётная запись \"%(username)s\" не имеет ни одной активной сессии"
#: models.py:238 models.py:255 models.py:272 views.py:684 views.py:1132
#: views.py:1175
msgid "NAS required"
@ -515,10 +510,6 @@ msgstr "Выделен из:"
msgid "From"
msgstr "От"
#: templates/abonapp/editAbon.html:218
msgid "Leases does not found"
msgstr "Аренды ip не найдены"
#: templates/abonapp/editAbon.html:230
#: templates/abonapp/modal_add_lease.html:16
#: templates/abonapp/modal_add_phone.html:27
@ -529,8 +520,8 @@ msgid "Add"
msgstr "Добавить"
#: templates/abonapp/editAbon.html:234
msgid "Active networks"
msgstr "Активные подсети"
msgid "Networks"
msgstr "Подсети"
#: templates/abonapp/editAbon.html:242
msgid "User flags"
@ -1159,3 +1150,6 @@ msgstr "Автопродление услуги."
msgid "No author attached"
msgstr "Автор не назначен"
msgid "User not have ip"
msgstr "У пользователя нет ip"

51
abonapp/migrations/0006_change_ip.py

@ -0,0 +1,51 @@
# Generated by Django 2.1 on 2018-10-10 16:11
# from json import dump
from django.db import migrations, models
TMP_FILE = '/tmp/migrate_ip.json'
DUMP = []
def backup_info(apps, _):
Abon = apps.get_model('abonapp', 'Abon')
abons = Abon.objects.annotate(
addr_count=models.Count('ip_addresses')
).filter(addr_count__gt=0).only('ip_addresses').iterator()
global DUMP
for abon in abons:
ip_addr = abon.ip_addresses.first()
DUMP.append({
'pk': abon.pk,
'addr': ip_addr.ip
})
# with open(TMP_FILE, 'w') as f:
# dump(r, f, indent=2)
def restore_ips(apps, _):
Abon = apps.get_model('abonapp', 'Abon')
for abon in DUMP:
Abon.objects.filter(pk=abon.get('pk')).update(ip_address=abon.get('addr'))
class Migration(migrations.Migration):
dependencies = [
('abonapp', '0005_current_tariff'),
]
operations = [
migrations.RunPython(backup_info),
migrations.RemoveField(
model_name='abon',
name='ip_addresses',
),
migrations.AddField(
model_name='abon',
name='ip_address',
field=models.GenericIPAddressField(blank=True, null=True, unique=True, verbose_name='Ip address'),
),
migrations.RunPython(restore_ips)
]

61
abonapp/models.py

@ -1,5 +1,4 @@
from datetime import datetime
from ipaddress import ip_address
from typing import Optional
from django.conf import settings
@ -13,10 +12,10 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, gettext
from accounts_app.models import UserProfile, MyUserManager, BaseAccount
from nas_app.nas_managers import AbonStruct, TariffStruct, NasFailedResult, NasNetworkError
from nas_app.nas_managers import SubnetQueue, NasFailedResult, NasNetworkError
from group_app.models import Group
from djing.lib import LogicError
from ip_pool.models import IpLeaseModel, NetworkModel
from ip_pool.models import NetworkModel
from tariff_app.models import Tariff, PeriodicPay
from bitfield import BitField
@ -90,7 +89,7 @@ class Abon(BaseAccount):
current_tariff = models.OneToOneField(AbonTariff, null=True, blank=True, on_delete=models.SET_NULL, default=None)
group = models.ForeignKey(Group, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('User group'))
ballance = models.FloatField(default=0.0)
ip_addresses = models.ManyToManyField(IpLeaseModel, verbose_name=_('Ip addresses'))
ip_address = models.GenericIPAddressField(verbose_name=_('Ip address'), unique=True, null=True, blank=True)
description = models.TextField(_('Comment'), null=True, blank=True)
street = models.ForeignKey(AbonStreet, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Street'))
house = models.CharField(_('House'), max_length=12, null=True, blank=True)
@ -198,6 +197,25 @@ class Abon(BaseAccount):
comment=comment or _('Buy service default log')
)
def attach_ip_addr(self, ip, strict=False):
"""
Attach ip address to account
:param ip: Instance of str or ip_address
:param strict: If strict is True then ip not replaced quietly
:return: None
"""
if strict and self.ip_address:
raise LogicError('Ip address already exists')
self.ip_address = ip
self.save(update_fields=('ip_address',))
def free_ip_addr(self) -> bool:
if self.ip_address:
self.ip_address = None
self.save(update_fields=('ip_address',))
return True
return False
# is subscriber have access to service, view in tariff_app.custom_tariffs.<TariffBase>.manage_access()
def is_access(self) -> bool:
if not self.is_active:
@ -210,22 +228,19 @@ class Abon(BaseAccount):
return ct.manage_access(self)
# make subscriber from agent structure
def build_agent_struct(self, raise_errs=True):
abon_addresses = tuple(ip_address(i.ip) for i in self.ip_addresses.filter(is_active=True))
# if not abon_addresses:
# return
def build_agent_struct(self):
if not self.ip_address:
return
abon_tariff = self.active_tariff()
if abon_tariff is None:
agent_trf = None
else:
trf = abon_tariff.tariff
agent_trf = TariffStruct(trf.id, trf.speedIn, trf.speedOut)
if len(abon_addresses) > 0:
return AbonStruct(self.pk, abon_addresses, agent_trf, self.is_access())
if raise_errs:
raise LogicError(_('Account "%(username)s" not have any active leases') % {
'username': self.username
})
if abon_tariff:
abon_tariff = abon_tariff.tariff
return SubnetQueue(
name="uid%d" % self.pk,
network=self.ip_address,
max_limit=(abon_tariff.speedIn, abon_tariff.speedOut),
queue_type=SubnetQueue.QUEUE_LEAF,
is_access=self.is_access()
)
def nas_sync_self(self) -> Optional[Exception]:
"""
@ -281,14 +296,6 @@ class Abon(BaseAccount):
def get_absolute_url(self):
return resolve_url('abonapp:abon_home', self.group.id, self.username)
def add_lease(self, ip: str, network: Optional[NetworkModel], mac_addr=None):
existed_client_ips = tuple(l.ip for l in self.ip_addresses.all())
if ip not in existed_client_ips:
lease = IpLeaseModel.objects.create_from_ip(ip=ip, net=network, mac=mac_addr)
if lease is None:
return 'Error while creating a ip lease'
self.ip_addresses.add(lease)
def enable_service(self, tariff: Tariff, deadline=None, time_start=None):
"""
Makes a services for current user

83
abonapp/templates/abonapp/editAbon.html

@ -46,9 +46,6 @@
<script type="text/javascript">
$(function () {
$("#iprefreshbtn").on('click', function(){
$("#{{ form.ip_address.id_for_label }}").val('');
});
$('#passwdtoggler').on('mousedown', function(){
document.getElementById("{{ form.password.id_for_label }}").type='text';
}).on('mouseup', function(){
@ -182,61 +179,45 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans 'Ip addresses' %}</h3>
<h3 class="panel-title">{% trans 'Ip address' %}</h3>
</div>
<div class="panel-body">
{% if abon.ip_address %}
<div class="btn-group btn-group-xs">
<a href="{% url 'abonapp:user_session_free' group.pk abon.username %}" class="btn btn-danger" title="{% trans 'Free session' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-remove"></span>
</a>
<b>{{ abon.ip_address }}</b>
{% if perms.abonapp.can_ping %}
<a href="{% url 'abonapp:ping' group.pk abon.username %}" class="btn btn-default btn-cmd" title="Ping" data-param="{{ abon.ip_address }}">
<span class="glyphicon glyphicon-flash"></span> Ping
</a>
{% else %}
<a href="#" class="btn btn-default disabled" title="{% trans 'Permission denied' %}">
<span class="glyphicon glyphicon-flash"></span> Ping
</a>
{% endif %}
</div>
{% else %}
<span class="text-info">{% trans 'No ip address' %}</span>
{% endif %}
</div>
<ul class="list-group">
{% with can_ping=perms.abonapp.can_ping %}
{% for lease in abon.ip_addresses.all %}
<li class="list-group-item">
<div class="btn-group btn-group-xs">
{% if lease.is_active %}
<a href="{% url 'abonapp:user_session_free' group.pk abon.username lease.pk %}" class="btn btn-danger" title="{% trans 'Free session' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% else %}
<a href="{% url 'abonapp:user_session_start' group.pk abon.username lease.pk %}" class="btn btn-success" title="{% trans 'Start session' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-flash"></span>
</a>
{% endif %}
{% if can_ping %}
<a href="{% url 'abonapp:ping' group.pk abon.username %}" class="btn btn-default btn-cmd" title="Ping" data-param="{{ lease.ip }}">
<span class="glyphicon glyphicon-flash"></span> Ping
</a>
{% else %}
<a href="#" class="btn btn-default disabled" title="{% trans 'Permission denied' %}">
<span class="glyphicon glyphicon-flash"></span> Ping
</a>
{% endif %}
</div>
<span{{ lease.is_active|yesno:', class="text-muted"'|safe }}>
<b>{{ lease }}</b>
<small>{% trans 'Leased by:' %} {{ lease.lease_time|date:'d-m H:i:s' }}.
{% if lease.mac_addr %}
{% trans 'From' %}: {{ lease.mac_addr }}.
{% endif %}
</small>
</span>
</li>
{% empty %}
<li class="list-group-item">{% trans 'Leases does not found' %}</li>
{% endfor %}
{% endwith %}
</ul>
<div class="panel-footer">
<div class="btn-group btn-group-sm">
{% if abon.is_dynamic_ip %}
<a href="#" class="btn btn-success" disabled>
{% if abon.ip_address %}
<a href="{% url 'abonapp:update_ip' group.pk abon.username %}" class="btn btn-primary btn-modal">
<span class="glyphicon glyphicon-edit"></span>
<span class="hidden-xs">{% trans 'Change' %}</span>
</a>
{% else %}
<a href="{% url 'abonapp:lease_add' group.pk abon.username %}" class="btn btn-success btn-modal">
<a href="{% url 'abonapp:update_ip' group.pk abon.username %}" class="btn btn-success btn-modal">
<span class="glyphicon glyphicon-plus"></span>
<span class="hidden-xs">{% trans 'Add' %}</span>
</a>
{% endif %}
<span class="glyphicon glyphicon-plus"></span>
<span class="hidden-xs">{% trans 'Add' %}</span>
</a>
<a href="{% url 'abonapp:active_nets' group.pk %}" class="btn btn-default btn-modal">
<span class="glyphicon glyphicon-globe"></span>
<span class="hidden-sm hidden-xs">{% trans 'Active networks' %}</span>
<span class="hidden-sm hidden-xs">{% trans 'Networks' %}</span>
</a>
</div>
</div>

24
abonapp/templates/abonapp/modal_add_lease.html

@ -1,24 +0,0 @@
{% extends request.is_ajax|yesno:'nullcont.htm,abonapp/ext.htm' %}
{% load i18n bootstrap3 %}
{% block content %}
<form role="form" action="{% url 'abonapp:lease_add' group.id uname %}" method="post"> {% csrf_token %}
<div class="modal-header primary">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-compressed"></span>{% trans 'Add ip lease' %}</h4>
</div>
<div class="modal-body">
{% bootstrap_form form %}
<div class="btn-group">
<button type="submit" class="btn btn-success">
<span class="glyphicon glyphicon-plus"></span> {% trans 'Add' %}
</button>
<button type="reset" class="btn btn-default">
<span class="glyphicon glyphicon-remove-circle"></span> {% trans 'Reset' %}
</button>
</div>
</div>
</form>
{% endblock %}

27
abonapp/templates/abonapp/modal_ip_form.html

@ -0,0 +1,27 @@
{% extends request.is_ajax|yesno:'bajax.html,base.html' %}
{% load i18n bootstrap3 %}
{% block breadcrumb %}
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li><a href="{% url 'abonapp:group_list' %}">{% trans 'User groups' %}</a></li>
<li><a href="{% url 'abonapp:people_list' group.id %}">{{ group.title }}</a></li>
<li><a href="{% url 'abonapp:abon_home' group.id abon.username %}">{{ abon.fio }}</a></li>
<li class="active">{% trans 'Update ip address' %}</li>
</ol>
{% endblock %}
{% block main %}
<form role="form" action="{% url 'abonapp:update_ip' group.id object.username %}" method="post"> {% csrf_token %}
<div class="modal-header primary">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-plus"></span>{% trans 'Update ip address' %}</h4>
</div>
<div class="modal-body">
{% bootstrap_form form %}
<button type="submit" class="btn btn-sm btn-primary">
<span class="glyphicon glyphicon-save"></span> {% trans 'Save' %}
</button>
</div>
</form>
{% endblock %}

7
abonapp/urls.py

@ -26,14 +26,13 @@ subscriber_patterns = [
path('tel/add/', views.tel_add, name='telephone_new'),
path('tel/del/', views.tel_del, name='telephone_del'),
path('markers/', views.EditSibscriberMarkers.as_view(), name='markers_edit'),
path('session/<int:lease_id>/free/', views.user_session_toggle, {'action': 'free'}, name='user_session_free'),
path('session/<int:lease_id>/start/', views.user_session_toggle, {'action': 'start'}, name='user_session_start'),
path('session/free/', views.user_session_free, name='user_session_free'),
path('periodic_pay/', views.add_edit_periodic_pay, name='add_periodic_pay'),
path('periodic_pay/<int:periodic_pay_id>/', views.add_edit_periodic_pay, name='add_periodic_pay'),
path('periodic_pay/<int:periodic_pay_id>/del/', views.del_periodic_pay, name='del_periodic_pay'),
path('lease/add/', views.lease_add, name='lease_add'),
path('ping/', views.abon_ping, name='ping'),
path('set_auto_continue_service/', views.set_auto_continue_service, name='set_auto_continue_service')
path('set_auto_continue_service/', views.set_auto_continue_service, name='set_auto_continue_service'),
path('update_ip/', views.IpUpdateView.as_view(), name='update_ip')
]
group_patterns = [

191
abonapp/views.py

@ -1,4 +1,3 @@
from ipaddress import ip_address
from typing import Dict, Optional
from datetime import datetime, date
from django.core.exceptions import PermissionDenied, ValidationError
@ -6,6 +5,7 @@ from django.db import IntegrityError, ProgrammingError, transaction, Operational
from django.db.models import Count, Q
from django.shortcuts import render, redirect, get_object_or_404, resolve_url
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin as PermissionRequiredMixin_django
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.contrib import messages
from django.urls import reverse_lazy
@ -25,21 +25,22 @@ from taskapp.models import Task
from dialing_app.models import AsteriskCDR
from statistics.models import getModel
from group_app.models import Group
from ip_pool.models import IpLeaseModel, NetworkModel
from ip_pool.forms import LeaseForm
from ip_pool.models import NetworkModel
from guardian.shortcuts import get_objects_for_user, assign_perm
from guardian.decorators import permission_required_or_403 as permission_required
from guardian.mixins import PermissionRequiredMixin
from djing import ping
from djing import lib
from djing.lib.decorators import json_view, only_admins
from djing.lib.mixins import OnlyAdminsMixin
from djing.global_base_views import OrderedFilteredList, SecureApiView
login_decs = login_required, only_admins
class AbonappPermissionMixin(LoginRequiredMixin, OnlyAdminsMixin, PermissionRequiredMixin):
return_403 = True
@method_decorator(login_decs, name='dispatch')
class PeoplesListView(OrderedFilteredList):
class PeoplesListView(LoginRequiredMixin, OnlyAdminsMixin, OrderedFilteredList):
template_name = 'abonapp/peoples.html'
def get_queryset(self):
@ -77,8 +78,7 @@ class PeoplesListView(OrderedFilteredList):
return context
@method_decorator(login_decs, name='dispatch')
class GroupListView(OrderedFilteredList):
class GroupListView(LoginRequiredMixin, OnlyAdminsMixin, OrderedFilteredList):
context_object_name = 'groups'
template_name = 'abonapp/group_list.html'
queryset = Group.objects.annotate(usercount=Count('abon'))
@ -90,9 +90,8 @@ class GroupListView(OrderedFilteredList):
return queryset
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.add_abon'), name='dispatch')
class AbonCreateView(CreateView):
class AbonCreateView(LoginRequiredMixin, OnlyAdminsMixin, PermissionRequiredMixin_django, CreateView):
permission_required = 'abonapp.add_abon'
group = None
abon = None
form_class = forms.AbonForm
@ -146,9 +145,8 @@ class AbonCreateView(CreateView):
return super(AbonCreateView, self).form_invalid(form)
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.delete_abon'), name='dispatch')
class DelAbonDeleteView(DeleteView):
class DelAbonDeleteView(AbonappPermissionMixin, DeleteView):
permission_required = 'abonapp.delete_abon'
model = models.Abon
slug_url_kwarg = 'uname'
slug_field = 'username'
@ -220,12 +218,14 @@ def abonamount(request, gid: int, uname):
})
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('group_app.view_group', (Group, 'pk', 'gid')), name='dispatch')
class DebtsListView(OrderedFilteredList):
class DebtsListView(AbonappPermissionMixin, OrderedFilteredList):
permission_required = 'group_app.view_group'
context_object_name = 'invoices'
template_name = 'abonapp/invoiceForPayment.html'
def get_permission_object(self):
return self.abon.group
def get_queryset(self):
abon = get_object_or_404(models.Abon, username=self.kwargs.get('uname'))
self.abon = abon
@ -238,12 +238,16 @@ class DebtsListView(OrderedFilteredList):
return context
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('group_app.view_group', (Group, 'pk', 'gid')), name='dispatch')
class PayHistoryListView(OrderedFilteredList):
class PayHistoryListView(AbonappPermissionMixin, OrderedFilteredList):
permission_required = 'group_app.view_group'
context_object_name = 'pay_history'
template_name = 'abonapp/payHistory.html'
def get_permission_object(self):
if hasattr(self, 'abon'):
return self.abon.group
return models.Group.objects.filter(pk=self.kwargs.get('gid')).first()
def get_queryset(self):
abon = get_object_or_404(models.Abon, username=self.kwargs.get('uname'))
self.abon = abon
@ -283,9 +287,8 @@ def abon_services(request, gid: int, uname):
})
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.view_abon'), name='post')
class AbonHomeUpdateView(UpdateView):
class AbonHomeUpdateView(AbonappPermissionMixin, UpdateView):
permission_required = 'abonapp.view_abon'
model = models.Abon
form_class = forms.AbonForm
slug_field = 'username'
@ -469,9 +472,8 @@ def unsubscribe_service(request, gid: int, uname, abon_tariff_id: int):
return redirect('abonapp:abon_services', gid=gid, uname=uname)
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.view_abonlog'), name='dispatch')
class LogListView(ListView):
class LogListView(AbonappPermissionMixin, ListView):
permission_required = 'abonapp.view_abonlog'
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
http_method_names = ('get',)
context_object_name = 'logs'
@ -479,9 +481,8 @@ class LogListView(ListView):
model = models.AbonLog
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.view_invoiceforpayment'), name='dispatch')
class DebtorsListView(ListView):
class DebtorsListView(AbonappPermissionMixin, ListView):
permission_required = 'abonapp.view_invoiceforpayment'
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
http_method_names = ('get',)
context_object_name = 'invoices'
@ -489,14 +490,16 @@ class DebtorsListView(ListView):
queryset = models.InvoiceForPayment.objects.filter(status=True)
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('group_app.view_group', (Group, 'pk', 'gid')), name='dispatch')
class TaskLogListView(ListView):
class TaskLogListView(AbonappPermissionMixin, ListView):
permission_required = 'group_app.view_group'
paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10)
http_method_names = ('get',)
context_object_name = 'tasks'
template_name = 'abonapp/task_log.html'
def get_permission_object(self):
return self.abon.group
def get_queryset(self):
abon = get_object_or_404(models.Abon, username=self.kwargs.get('uname'))
self.abon = abon
@ -509,9 +512,8 @@ class TaskLogListView(ListView):
return context
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.view_passportinfo'), name='dispatch')
class PassportUpdateView(UpdateView):
class PassportUpdateView(AbonappPermissionMixin, UpdateView):
permission_required = 'abonapp.view_passportinfo'
form_class = forms.PassportForm
model = models.PassportInfo
template_name = 'abonapp/modal_passport_view.html'
@ -549,6 +551,28 @@ class PassportUpdateView(UpdateView):
return super(PassportUpdateView, self).get_context_data(**context)
class IpUpdateView(AbonappPermissionMixin, UpdateView):
permission_required = 'abonapp.change_abon'
form_class = forms.AddIpForm
model = models.Abon
slug_url_kwarg = 'uname'
slug_field = 'username'
template_name = 'abonapp/modal_ip_form.html'
def dispatch(self, request, *args, **kwargs):
try:
return super(IpUpdateView, self).dispatch(request, *args, **kwargs)
except lib.LogicError as e:
messages.error(request, e)
return self.render_to_response(self.get_context_data(**kwargs))
def get_context_data(self, **kwargs):
context = super(IpUpdateView, self).get_context_data(**kwargs)
context['group'] = self.object.group
context['abon'] = self.object
return context
@login_required
@only_admins
def chgroup_tariff(request, gid):
@ -775,8 +799,7 @@ def vcards(r):
return response
@method_decorator(login_decs, name='dispatch')
class DialsListView(OrderedFilteredList):
class DialsListView(LoginRequiredMixin, OnlyAdminsMixin, OrderedFilteredList):
context_object_name = 'logs'
template_name = 'abonapp/dial_log.html'
@ -1106,9 +1129,8 @@ def del_periodic_pay(request, gid: int, uname, periodic_pay_id):
return redirect('abonapp:abon_services', gid, uname)
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('abonapp.change_abon'), name='dispatch')
class EditSibscriberMarkers(UpdateView):
class EditSibscriberMarkers(AbonappPermissionMixin, UpdateView):
permission_required = 'abonapp.change_abon'
http_method_names = ('get', 'post')
template_name = 'abonapp/modal_user_markers.html'
form_class = forms.MarkersForm
@ -1142,78 +1164,17 @@ class EditSibscriberMarkers(UpdateView):
@login_required
@only_admins
@permission_required('abonapp.change_abon')
def user_session_toggle(request, gid: int, uname, lease_id: int, action=None):
def user_session_free(request, gid: int, uname):
abon = get_object_or_404(models.Abon, username=uname)
if abon.nas is None:
messages.error(request, _('NAS required'))
return redirect('abonapp:abon_home', gid, uname)
lease = abon.ip_addresses.get(pk=lease_id)
tm = abon.nas.get_nas_manager()
try:
if action == 'free':
try:
abon_nas_obj = abon.build_agent_struct()
tm.lease_free(abon_nas_obj, ip_address(lease.ip))
messages.success(request, _('Ip lease has been freed'))
lease.free()
except lib.LogicError:
messages.error(request, _('You cannot disable last session'))
elif action == 'start':
lease.start()
abon_nas_obj = abon.build_agent_struct()
tm.lease_start(abon_nas_obj, ip_address(lease.ip))
messages.success(request, _('Ip lease has been started'))
else:
messages.error(request, _('Unexpected action'))
except (NasFailedResult, lib.LogicError) as e:
messages.error(request, e)
return redirect('abonapp:abon_home', gid, uname)
@login_required
@only_admins
@permission_required('abonapp.change_abon')
def lease_add(request, gid: int, uname):
group = get_object_or_404(Group, pk=gid)
if request.method == 'POST':
frm = LeaseForm(request.POST)
if frm.is_valid():
try:
abon = get_object_or_404(models.Abon, username=uname)
cleaned = frm.clean()
ip = cleaned.get('ip_addr')
is_dynamic = cleaned.get('is_dynamic')
network_id = cleaned.get('possible_networks') # str(int)
network = get_object_or_404(NetworkModel, pk=network_id)
lease = IpLeaseModel.objects.create_from_ip(ip, net=network, is_dynamic=is_dynamic)
abon.ip_addresses.add(lease)
if abon.nas is None:
messages.error(request, _('NAS required'))
else:
tm = abon.nas.get_nas_manager()
tm.lease_start(
user=abon.build_agent_struct(),
lease=lease.ip
)
messages.success(request, _('Ip lease has been created'))
return redirect('abonapp:abon_home', gid, uname)
except lib.DuplicateEntry as e:
messages.error(request, e)
else:
messages.error(request, _('Check form errors'))
if abon.ip_address:
abon.free_ip_addr()
messages.success(request, _('Ip lease has been freed'))
else:
first_network = NetworkModel.objects.filter(groups=group).first()
if first_network is not None:
free_ip = IpLeaseModel.objects.get_free_ip(first_network)
initial = {'ip_addr': free_ip}
else:
initial = None
frm = LeaseForm(initial=initial)
return render(request, 'abonapp/modal_add_lease.html', {
'form': frm,
'group': group,
'uname': uname
})
messages.error(request, _('User not have ip'))
return redirect('abonapp:abon_home', gid, uname)
@login_required
@ -1247,9 +1208,8 @@ def abons(request):
ablist = ({
'id': abn.pk,
'tarif_id': abn.active_tariff().tariff.pk if abn.active_tariff() is not None else 0,
'ips': ','.join(str(i.ip) for i in abn.ip_addresses.filter(is_active=True)),
'is_active': abn.is_active
} for abn in models.Abon.objects.all())
'ip': abn.ip_address
} for abn in models.Abon.objects.iterator())
tarlist = ({
'id': trf.pk,
@ -1285,10 +1245,13 @@ class DhcpLever(SecureApiView):
@method_decorator(json_view)
def get(self, request, *args, **kwargs):
data = request.GET.copy()
r = self.on_dhcp_event(data)
if r is not None:
return {'text': r}
return {'status': 'ok'}
try:
r = self.on_dhcp_event(data)
if r is not None:
return {'text': r}
return {'status': 'ok'}
except IntegrityError as e:
return {'status': str(e).replace('\n', ' ')}
@staticmethod
def on_dhcp_event(data: Dict) -> Optional[str]:

6
accounts_app/templatetags/acc_tags.py

@ -3,7 +3,7 @@ from ipaddress import ip_address, AddressValueError
from django import template
from django.db.models import Model
from django.apps import apps
from ip_pool.models import IpLeaseModel
from abonapp.models import Abon
from six import string_types, class_types
register = template.Library()
@ -26,8 +26,8 @@ def can_login_by_location(request):
try:
remote_ip = ip_address(request.META.get('REMOTE_ADDR'))
if remote_ip.version == 4:
has_leases = IpLeaseModel.objects.filter(ip=str(remote_ip), abon__is_active=True).exists()
return has_leases
has_exist = Abon.objects.filter(ip_address=str(remote_ip), is_active=True).exists()
return has_exist
except AddressValueError:
pass
return False

37
agent/commands/dhcp.py

@ -2,7 +2,6 @@ from typing import Optional
from django.core.exceptions import MultipleObjectsReturned
from abonapp.models import Abon
from devapp.models import Device, Port
from ip_pool.models import IpLeaseModel
def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: int) -> Optional[str]:
@ -13,22 +12,17 @@ def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: i
if mngr_class.get_is_use_device_port():
abon = Abon.objects.get(dev_port__device=dev,
dev_port__num=switch_port,
device=dev)
device=dev, is_active=True)
else:
abon = Abon.objects.get(device=dev)
abon = Abon.objects.get(device=dev, is_active=True)
if not abon.is_dynamic_ip:
return 'User settings is not dynamic'
client_ips = tuple(str(ip) for ip in abon.ip_addresses.all())
if client_ip in client_ips:
return 'Ip address already existed'
add_lease_result = abon.add_lease(client_ip, mac_addr=client_mac, network=None)
if add_lease_result is None:
if abon.is_access():
abon.nas_sync_self()
else:
return 'User %s is not access to service' % abon.username
abon.attach_ip_addr(client_ip, strict=False)
if abon.is_access():
r = abon.nas_sync_self()
return r if r else None
else:
return add_lease_result
return 'User %s is not access to service' % abon.username
except Abon.DoesNotExist:
return "User with device with mac '%s' does not exist" % switch_mac
except Device.DoesNotExist:
@ -43,16 +37,13 @@ def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: i
def dhcp_expiry(client_ip: str) -> Optional[str]:
try:
lease = IpLeaseModel.objects.get(ip=client_ip)
lease.is_active = False
lease.save(update_fields=('is_active',))
abon = Abon.objects.filter(ip_addresses=lease).first()
if abon is None:
return "Subscriber with ip %s does not exist" % client_ip
abon.nas_sync_self()
except IpLeaseModel.DoesNotExist:
pass
abon = Abon.objects.filter(ip_address=client_ip, is_active=True).exclude(current_tariff=None).first()
if abon is None:
return "Subscriber with ip %s does not exist" % client_ip
else:
is_freed = abon.free_ip_addr()
if is_freed:
abon.nas_sync_self()
def dhcp_release(client_ip: str) -> Optional[str]:

2
dhcp_lever.py

@ -5,7 +5,7 @@ from urllib.parse import urlencode
from urllib.request import urlopen
from hashlib import sha256
API_AUTH_SECRET = 'your api key'
API_AUTH_SECRET = 'yourapikey'
SERVER_DOMAIN = 'http://localhost:8000'

8
djing/lib/auth_backends.py

@ -3,7 +3,6 @@ from ipaddress import ip_address, AddressValueError
from django.contrib.auth.backends import ModelBackend
from accounts_app.models import BaseAccount, UserProfile
from abonapp.models import Abon
from ip_pool.models import IpLeaseModel
class CustomAuthBackend(ModelBackend):
@ -40,13 +39,12 @@ class LocationAuthBackend(ModelBackend):
def authenticate(self, request, byip, **kwargs):
try:
remote_ip = ip_address(request.META.get('REMOTE_ADDR'))
lease = IpLeaseModel.objects.filter(ip=str(remote_ip), abon__is_active=True).first()
if lease is None:
user = Abon.objects.filter(ip_address=str(remote_ip), is_active=True).first()
if user is None:
return
user = Abon.objects.get(ip_addresses=lease)
if self.user_can_authenticate(user):
return user
except (AddressValueError, Abon.DoesNotExist):
except AddressValueError:
return
def get_user(self, user_id):

9
djing/lib/mixins.py

@ -0,0 +1,9 @@
from django.contrib.auth.mixins import AccessMixin
class OnlyAdminsMixin(AccessMixin):
"""Verify that the current user is admin."""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_admin:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)

1
ip_pool/admin.py

@ -2,4 +2,3 @@ from django.contrib import admin
from ip_pool import models
admin.site.register(models.NetworkModel)
admin.site.register(models.IpLeaseModel)

38
ip_pool/forms.py

@ -1,14 +1,12 @@
from ipaddress import ip_network, ip_address
from ipaddress import ip_network
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from ip_pool import models
class NetworkForm(forms.ModelForm):
def clean_network(self):
netw = self.data.get('network')
if netw is None:
@ -22,32 +20,8 @@ class NetworkForm(forms.ModelForm):
class Meta:
model = models.NetworkModel
fields = '__all__'
class LeaseForm(forms.Form):
def __init__(self, data=None, *args, **kwargs):
super(LeaseForm, self).__init__(data=data, *args, **kwargs)
nets = models.NetworkModel.objects.defer('groups')
if nets.exists():
self.fields['possible_networks'].choices = ((net.pk, str(net.get_network())) for net in nets.iterator())
def clean_ip_addr(self):
ip_addr = self.data.get('ip_addr')
if ip_addr is None:
return
ip_addr = ip_address(ip_addr)
net_id = self.data.get('possible_networks')
if net_id is None:
return ip_addr.compressed
net = models.NetworkModel.objects.get(pk=net_id)
if ip_addr not in net.get_network():
raise ValidationError(_('Ip that you typed is not in subnet that you have selected'))
if ip_addr < ip_address(net.ip_start):
raise ValidationError(_('Ip that you have passed is less than allowed network range'))
if ip_addr > ip_address(net.ip_end):
raise ValidationError(_('Ip that you have passed is greater than allowed network range'))
return ip_addr.compressed
ip_addr = forms.GenericIPAddressField(label=_('Ip address'))
is_dynamic = forms.BooleanField(label=_('Is dynamic'), required=False)
possible_networks = forms.ChoiceField(label=_('Possible networks'))
widgets = {
'groups': forms.SelectMultiple(attrs={
'size': 12
})
}

43
ip_pool/migrations/0003_auto_20181015_1430.py

@ -0,0 +1,43 @@
# Generated by Django 2.1 on 2018-10-15 14:30
from django.db import migrations, models
import djing.fields
class Migration(migrations.Migration):
dependencies = [
('ip_pool', '0002_change_unique'),
]
operations = [
migrations.CreateModel(
name='LeasesHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip', models.GenericIPAddressField(verbose_name='Ip address')),
('lease_time', models.DateTimeField(auto_now_add=True, verbose_name='Lease time')),
('mac_addr', djing.fields.MACAddressField(blank=True, integer=True, null=True, verbose_name='Mac address')),
],
options={
'verbose_name': 'History lease',
'verbose_name_plural': 'Leases history',
'db_table': 'ip_pool_leases_history',
'ordering': ('-lease_time',),
},
),
migrations.RemoveField(
model_name='ipleasemodel',
name='is_active',
),
migrations.RemoveField(
model_name='ipleasemodel',
name='is_dynamic',
),
migrations.AddField(
model_name='networkmodel',
name='speed',
field=models.FloatField(default=0.0, verbose_name='Speed for subnet'),
preserve_default=False,
),
]

61
ip_pool/models.py

@ -1,6 +1,6 @@
from datetime import timedelta
from ipaddress import ip_network, ip_address
from typing import Optional
from typing import Optional, Generator
from django.conf import settings
from django.db.utils import IntegrityError
@ -39,6 +39,8 @@ class NetworkModel(models.Model):
ip_start = models.GenericIPAddressField(_('Start work ip range'))
ip_end = models.GenericIPAddressField(_('End work ip range'))
speed = models.FloatField(_('Speed for subnet'))
def __str__(self):
netw = self.get_network()
return "%s: %s" % (self.description, netw.with_prefixlen)
@ -105,6 +107,32 @@ class NetworkModel(models.Model):
return _('Unspecified')
return "I don't know"
def get_free_ip(self, employed_ips: Optional[Generator]):
"""
Find free ip in network.
:param employed_ips: Sorted from less to more ip addresses from current network.
:return: single finded ip
"""
network = self.get_network()
work_range_start_ip = ip_address(self.ip_start)
work_range_end_ip = ip_address(self.ip_end)
if employed_ips is None:
for ip in network.hosts():
if work_range_start_ip <= ip <= work_range_end_ip:
return ip
return
for ip in network.hosts():
if ip < work_range_start_ip:
continue
elif ip > work_range_end_ip:
break # Not found
used_ip = next(employed_ips)
if used_ip is None:
return ip
used_ip = ip_address(used_ip)
if ip < used_ip:
return ip
class Meta:
db_table = 'ip_pool_network'
verbose_name = _('Network')
@ -145,7 +173,6 @@ class IpLeaseManager(models.Manager):
ip=ip,
network=net,
is_dynamic=is_dynamic,
is_active=True,
mac_addr=mac
)
except IntegrityError as e:
@ -159,14 +186,13 @@ class IpLeaseManager(models.Manager):
return self.filter(lease_time__lt=senility)
# Deprecated. Remove after migrations squashed
class IpLeaseModel(models.Model):
ip = models.GenericIPAddressField(verbose_name=_('Ip address'), unique=True)
network = models.ForeignKey(NetworkModel, on_delete=models.CASCADE,
verbose_name=_('Parent network'), null=True, blank=True)
mac_addr = MACAddressField(verbose_name=_('Mac address'), null=True, blank=True)
lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True)
is_dynamic = models.BooleanField(_('Is dynamic'), default=False)
is_active = models.BooleanField(_('Is active'), default=True)
device_info = models.CharField(null=True, blank=True, default=None, max_length=128)
objects = IpLeaseManager()
@ -174,16 +200,6 @@ class IpLeaseModel(models.Model):
def __str__(self):
return self.ip
def free(self):
if self.is_active:
self.is_active = False
self.save(update_fields=('is_active',))
def start(self):
if not self.is_active:
self.is_active = True
self.save(update_fields=('is_active',))
def clean(self):
ip = ip_address(self.ip)
network = self.network.get_network()
@ -201,7 +217,16 @@ class IpLeaseModel(models.Model):
unique_together = ('ip', 'network', 'mac_addr')
# class LeasesHistory(models.Model):
# ip = models.GenericIPAddressField(verbose_name=_('Ip address'))
# lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True)
# mac_addr = MACAddressField(_('Mac address'), null=True, blank=True)
class LeasesHistory(models.Model):
ip = models.GenericIPAddressField(verbose_name=_('Ip address'))
lease_time = models.DateTimeField(_('Lease time'), auto_now_add=True)
mac_addr = MACAddressField(_('Mac address'), null=True, blank=True)
def __str__(self):
return self.ip
class Meta:
db_table = 'ip_pool_leases_history'
verbose_name = _('History lease')
verbose_name_plural = _('Leases history')
ordering = '-lease_time',

44
ip_pool/templates/ip_pool/ip_leases_list.html

@ -1,44 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block breadcrumb %}
<ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li>
<li><a href="{% url 'ip_pool:networks' %}">{% trans 'Ip pool' %}</a></li>
<li><a href="{% url 'ip_pool:net_edit' net.id %}">{{ net }}</a></li>
<li class="active">{% trans 'Ip leases list' %}</li>
</ol>
{% endblock %}
{% block page-header %}
{% trans 'Ip leases list' %}
{% endblock %}
{% block main %}
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="col-sm-5">{% trans 'Ip' %}</th>
<th class="col-sm-3">{% trans 'Lease time' %}</th>
<th class="col-sm-3">{% trans 'Network' %}</th>
<th class="col-sm-1">{% trans 'Is dynamic' %}</th>
</tr>
</thead>
<tbody>
{% for ip in object_list %}
<tr>
<td>{{ ip.ip }}</td>
<td>{{ ip.lease_time|date:'j:n H:i:s' }}</td>
<td>{{ ip.get_network }}</td>
<td><input type="checkbox" {{ ip.is_dynamic|yesno:'checked,' }}></td>
</tr>
{% empty %}
<tr>
<td colspan="4">{% trans 'You have not any available dedicated ips in this network' %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

4
ip_pool/templates/ip_pool/net_edit.html

@ -38,10 +38,6 @@
<a href="{% back_url request %}" class="btn btn-default">
<span class="glyphicon glyphicon-backward"></span> {% trans 'Back' %}
</a>
<a href="{% url 'ip_pool:ip_leases_list' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-eye-open"></span>
<span class="hidden-xs hidden-sm">{% trans 'View employed' %}</span>
</a>
<a href="{% url 'ip_pool:net_groups' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-user"></span>
<span class="hidden-xs hidden-sm">{% trans 'Groups available' %}</span>

6
ip_pool/templates/ip_pool/network_list.html

@ -28,7 +28,7 @@
{% with can_ch_net=perms.ip_pool.change_networkmodel can_del_net=perms.ip_poo.delete_networkmodel %}
{% for netw in networks_list %}
<tr>
<td><a href="{% url 'ip_pool:ip_leases_list' netw.id %}">{{ netw.get_network }}</a></td>
<td>{{ netw.get_network }}</td>
<td>{{ netw.get_kind_display }}</td>
<td>{{ netw.description }}</td>
<td>{{ netw.get_scope }}</td>
@ -55,10 +55,6 @@
<span class="hidden-xs hidden-sm">{% trans 'Permission denied' %}</span>
</a>
{% endif %}
<a href="{% url 'ip_pool:ip_leases_list' netw.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-eye-open"></span>
<span class="hidden-xs hidden-sm">{% trans 'View employed' %}</span>
</a>
</td>
</tr>
{% empty %}

1
ip_pool/urls.py

@ -7,7 +7,6 @@ app_name = 'ip_pool'
urlpatterns = [
path('', views.NetworksListView.as_view(), name='networks'),
path('network_add/', views.NetworkCreateView.as_view(), name='net_add'),
path('<int:net_id>/', views.IpLeasesListView.as_view(), name='ip_leases_list'),
path('<int:net_id>/edit/', views.NetworkUpdateView.as_view(), name='net_edit'),
path('<int:net_id>/del/', views.NetworkDeleteView.as_view(), name='net_delete'),
path('<int:net_id>/group_attach/', views.network_in_groups, name='net_groups')

17
ip_pool/views.py

@ -57,23 +57,6 @@ class NetworkDeleteView(DeleteView):
return super(NetworkDeleteView, self).delete(request, *args, **kwargs)
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('ip_pool.view_ipleasemodel'), name='dispatch')
class IpLeasesListView(OrderedFilteredList):
template_name = 'ip_pool/ip_leases_list.html'
model = models.IpLeaseModel
def get_context_data(self, **kwargs):
net_id = self.kwargs.get('net_id')
context = super().get_context_data(**kwargs)
context['net'] = get_object_or_404(models.NetworkModel, pk=net_id)
return context
def get_queryset(self):
net_id = self.kwargs.get('net_id')
return self.model.objects.filter(network__id=net_id)
@method_decorator(login_decs, name='dispatch')
@method_decorator(permission_required('ip_pool.add_networkmodel'), name='dispatch')
class NetworkCreateView(CreateView):

2
nas_app/nas_managers/__init__.py

@ -1,6 +1,6 @@
from nas_app.nas_managers.mod_mikrotik import MikrotikTransmitter
from nas_app.nas_managers.core import NasNetworkError, NasFailedResult
from nas_app.nas_managers.structs import TariffStruct, AbonStruct
from nas_app.nas_managers.structs import SubnetQueue
# Указываем какие реализации NAS у нас есть, это будет использоваться в
# web интерфейсе

80
nas_app/nas_managers/core.py

@ -1,7 +1,7 @@
from abc import ABC, abstractmethod, abstractproperty
from typing import Iterator, Any, Tuple, Optional
from djing import ping
from nas_app.nas_managers.structs import AbonStruct, TariffStruct, VectorAbon, VectorTariff
from nas_app.nas_managers.structs import SubnetQueue, VectorQueue
# Raised if NAS has returned failed result
@ -33,66 +33,36 @@ class BaseTransmitter(ABC):
return cls.description
@abstractmethod
def add_user_range(self, user_list: VectorAbon):
def add_user_range(self, queue_list: VectorQueue):
"""add subscribers list to NAS
:param user_list: Vector of instances of subscribers
:param queue_list: Vector of instances of subscribers
"""
@abstractmethod
def remove_user_range(self, users: VectorAbon):
def remove_user_range(self, queues):
"""remove subscribers list
:param users: Vector of instances of subscribers
:param queues: Vector of instances of subscribers
"""
@abstractmethod
def add_user(self, user: AbonStruct, *args):
def add_user(self, queue: SubnetQueue, *args):
"""add subscriber
:param user: Subscriber instance
:param queue: Subscriber instance
"""
@abstractmethod
def remove_user(self, user: AbonStruct):
def remove_user(self, queue: SubnetQueue):
"""
remove subscriber
:param user: Subscriber instance
:param queue: Subscriber instance
"""
@abstractmethod
def update_user(self, user: AbonStruct, *args):
def update_user(self, queue: SubnetQueue, *args):
"""
Update subscriber by uid, you can change everything except its uid.
Subscriber will found by UID.
:param user: Subscriber instance
"""
@abstractmethod
def add_tariff_range(self, tariff_list: VectorTariff):
"""Add services list to NAS.
:param tariff_list: Vector of TariffStruct
"""
@abstractmethod
def remove_tariff_range(self, tariff_list: VectorTariff):
"""Remove tariff list by unique id list.
:param tariff_list: Vector of TariffStruct
"""
@abstractmethod
def add_tariff(self, tariff: TariffStruct):
pass
@abstractmethod
def update_tariff(self, tariff: TariffStruct):
"""
Update tariff by uid, you can change everything except its uid.
Tariff will found by UID.
:param tariff: Service for update
"""
@abstractmethod
def remove_tariff(self, tid: int):
"""
:param tid: unique id of tariff.
:param queue: Subscriber instance
"""
@abstractmethod
@ -105,33 +75,15 @@ class BaseTransmitter(ABC):
"""
@abstractmethod
def read_users(self) -> VectorAbon:
def read_users(self) -> VectorQueue:
pass
@abstractmethod
def lease_free(self, user: AbonStruct, lease):
"""
Remove ip lease from allowed to network
:param lease: ip_address for lease
:param user: Subscriber instance
:return:
"""
@abstractmethod
def lease_start(self, user: AbonStruct, lease):
"""
Starts ip lease to allowed to network
:param lease: ip_address for lease
:param user: Subscriber instance
:return:
"""
def _diff_users(self, users_from_db: Iterator[Any]) -> Tuple[set, set]:
"""
:param users_from_db: QuerySet of all subscribers that can have service
:return: Tuple of 2 lists that contain list to add users and list to remove users
"""
users_struct_gen = (ab.build_agent_struct(raise_errs=False) for ab in users_from_db if
users_struct_gen = (ab.build_agent_struct() for ab in users_from_db if
ab is not None and ab.is_access())
users_struct_set = set(ab for ab in users_struct_gen if ab is not None and ab.tariff is not None)
users_from_nas = set(self.read_users())
@ -153,3 +105,9 @@ class BaseTransmitter(ABC):
for la in list_for_add:
print('\t', la)
self.add_user_range(list_for_add)
def diff_set(one: set, two: set) -> Tuple[set, set]:
list_for_del = (one ^ two) - one
list_for_add = one - two
return list_for_add, list_for_del

388
nas_app/nas_managers/mod_mikrotik.py

@ -3,14 +3,15 @@ import re
import socket
from abc import ABCMeta
from hashlib import md5
from ipaddress import _BaseAddress, ip_address
from typing import Iterable, Optional, Tuple, Generator, Dict
from ipaddress import ip_network, _BaseNetwork
from typing import Iterable, Optional, Tuple, Generator, Dict, Iterator, Any
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from djing.lib.decorators import LazyInitMetaclass
from nas_app.nas_managers.core import BaseTransmitter, NasNetworkError, NasFailedResult
from nas_app.nas_managers.structs import TariffStruct, AbonStruct, VectorAbon, VectorTariff
from nas_app.nas_managers import core
from nas_app.nas_managers import structs as i_structs
from ip_pool.models import NetworkModel
DEBUG = getattr(settings, 'DEBUG', False)
@ -40,7 +41,8 @@ class ApiRos(object):
md.update(bytes(pwd, 'utf-8'))
md.update(chal)
for _ in self.talk_iter(("/login", "=name=" + username,
"=response=00" + binascii.hexlify(md.digest()).decode('utf-8'))):
"=response=00" + binascii.hexlify(
md.digest()).decode('utf-8'))):
pass
self.is_login = True
@ -100,12 +102,15 @@ class ApiRos(object):
self.write_bytes(bytes(((l >> 8) & 0xff, l & 0xff)))
elif l < 0x200000:
l |= 0xC00000
self.write_bytes(bytes(((l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff)))
self.write_bytes(
bytes(((l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff)))
elif l < 0x10000000:
l |= 0xE0000000
self.write_bytes(bytes(((l >> 24) & 0xff, (l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff)))
self.write_bytes(bytes(((l >> 24) & 0xff, (l >> 16) & 0xff,
(l >> 8) & 0xff, l & 0xff)))
else:
self.write_bytes(bytes((0xf0, (l >> 24) & 0xff, (l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff)))
self.write_bytes(bytes((0xf0, (l >> 24) & 0xff, (l >> 16) & 0xff,
(l >> 8) & 0xff, l & 0xff)))
def read_len(self):
c = self.read_bytes(1)[0]
@ -144,7 +149,7 @@ class ApiRos(object):
while n < len(s):
r = self.sk.send(s[n:])
if r == 0:
raise NasFailedResult("connection closed by remote end")
raise core.NasFailedResult("connection closed by remote end")
n += r
def read_bytes(self, length):
@ -152,7 +157,7 @@ class ApiRos(object):
while len(ret) < length:
s = self.sk.recv(length - len(ret))
if len(s) == 0:
raise NasFailedResult("connection closed by remote end")
raise core.NasFailedResult("connection closed by remote end")
ret += s
return ret
@ -162,19 +167,23 @@ class ApiRos(object):
self.sk.close()
class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs', (ABCMeta, LazyInitMetaclass), {})):
class MikrotikTransmitter(core.BaseTransmitter, ApiRos,
metaclass=type('_ABC_Lazy_mcs',
(ABCMeta, LazyInitMetaclass), {})):
description = _('Mikrotik NAS')
def __init__(self, login: str, password: str, ip: str, port: int, *args, **kwargs):
def __init__(self, login: str, password: str, ip: str, port: int, *args,
**kwargs):
try:
BaseTransmitter.__init__(self,
login=login, password=password, ip=ip,
port=port, *args, **kwargs
)
core.BaseTransmitter.__init__(self,
login=login, password=password,
ip=ip,
port=port, *args, **kwargs
)
ApiRos.__init__(self, ip, port)
self.login(username=login, pwd=password)
except ConnectionRefusedError:
raise NasNetworkError('Connection to %s is Refused' % ip)
raise core.NasNetworkError('Connection to %s is Refused' % ip)
def _exec_cmd(self, cmd: Iterable) -> Dict:
if not isinstance(cmd, (list, tuple)):
@ -184,7 +193,7 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
if k == '!done':
break
elif k == '!trap':
raise NasFailedResult(v.get('=message'))
raise core.NasFailedResult(v.get('=message'))
r[k] = v or None
return r
@ -195,12 +204,12 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
if k == '!done':
break
elif k == '!trap':
raise NasFailedResult(v.get('=message'))
raise core.NasFailedResult(v.get('=message'))
if v:
yield v
@staticmethod
def _build_shape_obj(info: Dict) -> AbonStruct:
def _build_shape_obj(info: Dict) -> i_structs.SubnetQueue:
# Переводим приставку скорости Mikrotik в Mbit/s
def parse_speed(text_speed):
text_speed_digit = float(text_speed[:-1] or 0.0)
@ -216,10 +225,8 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
return res
speed_out, speed_in = info['=max-limit'].split('/')
t = TariffStruct(
speed_in=parse_speed(speed_in),
speed_out=parse_speed(speed_out)
)
speed_in = parse_speed(speed_in)
speed_out = parse_speed(speed_out)
try:
target = info.get('=target')
if target is None:
@ -228,18 +235,26 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
disabled = info.get('=disabled', False)
if disabled is not None:
disabled = True if disabled == 'true' else False
if target is not None and name is not None:
if target and name:
# target may be '192.168.0.3/32,192.168.0.2/32'
ips = (ip.split('/')[0] for ip in target.split(','))
a = AbonStruct(
uid=int(name[3:]),
ips=ips,
tariff=t,
is_access=not disabled
)
if len(a.ips) < 1:
net = target.split(',')[0]
if not net:
return
a.queue_id = info.get('=.id')
a = i_structs.SubnetQueue(
name=name,
network=net,
max_limit=(speed_in, speed_out),
is_access=not disabled,
queue_id=info.get('=.id')
)
if name.startswith('uid'):
a.queue_type = i_structs.SubnetQueue.QUEUE_LEAF
elif name.startswith('net_'):
a.queue_type = i_structs.SubnetQueue.QUEUE_SUBNET
elif name == 'queue-root':
a.queue_type = i_structs.SubnetQueue.QUEUE_ROOT
else:
a.queue_type = i_structs.SubnetQueue.QUEUE_UNKNOWN
return a
except ValueError as e:
print('ValueError:', e)
@ -249,65 +264,65 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
#################################################
# Find queue by name
def find_queue(self, name: str) -> Optional[AbonStruct]:
def find_queue(self, name: str) -> Optional[i_structs.SubnetQueue]:
r = self._exec_cmd(('/queue/simple/print', '?name=%s' % name))
if r:
return self._build_shape_obj(r.get('!re'))
def add_queue(self, user: AbonStruct) -> None:
if not isinstance(user, AbonStruct):
raise TypeError
if user.tariff is None or not isinstance(user.tariff, TariffStruct):
return
ips = ','.join(str(i) for i in user.ips)
def add_queue(self, queue: i_structs.SubnetQueue,
parent_name: str) -> None:
if not isinstance(queue, i_structs.SubnetQueue):
raise TypeError('queue must be instance of SubnetQueue')
self._exec_cmd((
'/queue/simple/add',
'=name=uid%d' % user.uid,
'=name=%s' % queue.name,
# FIXME: тут в разных микротиках или =target-addresses или =target
'=target=%s' % ips,
'=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn),
'=target=%s' % queue.network,
'=max-limit=%.3fM/%.3fM' % queue.max_limit,
'=queue=Djing_SFQ/Djing_SFQ',
'=burst-time=1/1'
'=burst-time=1/1',
'=parent=%s' % parent_name,
'=total-queue=Djing_SFQ'
))
def remove_queue(self, user: AbonStruct, queue: AbonStruct = None) -> None:
if not isinstance(user, AbonStruct):
def remove_queue(self, queue: i_structs.SubnetQueue) -> None:
if not isinstance(queue, i_structs.SubnetQueue):
raise TypeError
if queue is None:
queue = self.find_queue('uid%d' % user.uid)
if not queue.queue_id:
queue = self.find_queue(queue.name)
if queue is not None:
queue_id = getattr(queue, 'queue_id')
if queue_id is not None:
if queue.queue_id:
self._exec_cmd((
'/queue/simple/remove',
'=.id=%s' % queue_id
'=.id=%s' % queue.queue_id
))
def remove_queue_range(self, q_ids: Iterable[str]):
self._exec_cmd(('/queue/simple/remove', '=numbers=' + ','.join(q_ids)))
ids = ','.join(q_ids)
if len(ids) > 1:
self._exec_cmd(('/queue/simple/remove', '=numbers=%s' % ids))
def update_queue(self, user: AbonStruct, queue=None):
if not isinstance(user, AbonStruct):
def update_queue(self, queue: i_structs.SubnetQueue, parent_name: str):
if not isinstance(queue, i_structs.SubnetQueue):
raise TypeError
if user.tariff is None:
return
if queue is None:
queue = self.find_queue('uid%d' % user.uid)
if not queue.queue_id:
queue = self.find_queue(queue.name)
if queue is None:
return self.add_queue(user)
return self.add_queue(queue, parent_name)
else:
mk_id = getattr(queue, 'queue_id')
cmd = [
'/queue/simple/set',
'=name=uid%d' % user.uid,
'=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn),
# FIXME: тут в разных версиях прошивки микротика или =target-addresses или =target
'=target=%s' % ','.join(str(i) for i in user.ips),
'=name=%s' % queue.name,
'=max-limit=%.3fM/%.3fM' % queue.max_limit,
# FIXME: тут в разных версиях прошивки микротика
# или =target-addresses или =target
'=target=%s' % queue.network,
'=queue=Djing_SFQ/Djing_SFQ',
'=parent=%s' % parent_name,
'=burst-time=1/1'
]
if mk_id is not None:
cmd.insert(1, '=.id=%s' % mk_id)
if queue.queue_id:
cmd.insert(1, '=.id=%s' % queue.queue_id)
r = self._exec_cmd(cmd)
return r
@ -321,13 +336,13 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
# Ip->firewall->address list
#################################################
def add_ip(self, list_name: str, ip):
if not issubclass(ip.__class__, _BaseAddress):
def add_ip(self, list_name: str, net):
if not issubclass(net.__class__, _BaseNetwork):
raise TypeError
commands = (
'/ip/firewall/address-list/add',
'=list=%s' % list_name,
'=address=%s' % ip
'=address=%s' % net
)
return self._exec_cmd(commands)
@ -343,107 +358,67 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
'=numbers=%s' % ','.join(ip_firewall_ids)
))
def find_ip(self, ip, list_name: str):
if not issubclass(ip.__class__, _BaseAddress):
def find_ip(self, net, list_name: str):
if not issubclass(net.__class__, _BaseNetwork):
raise TypeError
r = self._exec_cmd((
'/ip/firewall/address-list/print', 'where',
'?list=%s' % list_name,
'?address=%s' % ip
'?address=%s' % net
))
return r.get('!re')
def read_ips_iter(self, list_name: str) -> Generator:
ips = self._exec_cmd_iter((
def read_nets_iter(self, list_name: str) -> Generator:
nets = self._exec_cmd_iter((
'/ip/firewall/address-list/print', 'where',
'?list=%s' % list_name,
'?dynamic=no'
))
for dat in ips:
yield ip_address(dat.get('=address')), dat.get('=.id')
for dat in nets:
yield ip_network(dat.get('=address'), strict=False), dat.get(
'=.id')
#################################################
# BaseTransmitter implementation
#################################################
def add_user_range(self, user_list: VectorAbon):
for usr in user_list:
self.add_user(usr)
def add_user_range(self, queue_list: i_structs.VectorQueue):
for q in queue_list:
self.add_user(q)
def remove_user_range(self, users: VectorAbon):
if not isinstance(users, (tuple, list, set)):
def remove_user_range(self, queues: i_structs.VectorQueue):
if not isinstance(queues, (tuple, list, set)):
raise ValueError('*users* is used twice, generator does not fit')
queue_ids = (usr.queue_id for usr in users if usr is not None)
queue_ids = (q.queue_id for q in queues if q)
self.remove_queue_range(queue_ids)
for user in users:
if isinstance(user, AbonStruct):
for ip in user.ips:
ip_list_entity = self.find_ip(ip, LIST_USERS_ALLOWED)
if ip_list_entity:
self.remove_ip(ip_list_entity.get('=.id'))
def add_user(self, user: AbonStruct, *args):
if user.tariff is None:
return
if not isinstance(user.tariff, TariffStruct):
for q in queues:
if isinstance(q, i_structs.SubnetQueue):
ip_list_entity = self.find_ip(q.network, LIST_USERS_ALLOWED)
if ip_list_entity:
self.remove_ip(ip_list_entity.get('=.id'))
def add_user(self, queue: i_structs.SubnetQueue, parent_name=None, *args):
try:
self.add_queue(queue, parent_name=parent_name)
except core.NasFailedResult as e:
print('Error:', e)
net = queue.network
if not issubclass(net.__class__, _BaseNetwork):
raise TypeError
try:
self.add_queue(user)
except NasFailedResult as e:
self.add_ip(LIST_USERS_ALLOWED, net)
except core.NasFailedResult as e:
print('Error:', e)
for ip in user.ips:
if not issubclass(ip.__class__, _BaseAddress):
raise TypeError
try:
self.add_ip(LIST_USERS_ALLOWED, ip)
except NasFailedResult as e:
print('Error:', e)
def remove_user(self, user: AbonStruct):
self.remove_queue(user)
def _finder(ips):
for ip in ips:
r = self.find_ip(ip, LIST_USERS_ALLOWED)
if r: yield r.get('=.id')
firewall_ip_list_ids = _finder(user.ips)
self.remove_ip_range(firewall_ip_list_ids)
def update_user(self, user: AbonStruct, *args):
# queue is instance of AbonStruct
queue = self.find_queue('uid%d' % user.uid)
for ip in user.ips:
if not issubclass(ip.__class__, _BaseAddress):
raise TypeError
nas_ip = self.find_ip(ip, LIST_USERS_ALLOWED)
if user.is_access:
if nas_ip is None:
self.add_ip(LIST_USERS_ALLOWED, ip)
else:
# если не активен - то и обновлять не надо
# но и выключить на всяк случай надо, а то вдруг был включён
if nas_ip:
# и если найден был - то удалим ip из разрешённых
self.remove_ip(nas_ip.get('=.id'))
if queue is not None:
self.remove_queue(user, queue)
queue = None
# если нет услуги то её не должно быть и в nas
if user.tariff is None:
if queue is not None:
self.remove_queue(user, queue)
return
if not user.is_access:
return
# Проверяем шейпер
if queue is None:
self.add_queue(user)
return
if queue != user:
self.update_queue(user, queue)
def remove_user(self, queue: i_structs.SubnetQueue):
self.remove_queue(queue)
r = self.find_ip(queue.network, LIST_USERS_ALLOWED)
ip_id = r.get('=.id')
self.remove_ip(ip_id)
def update_user(self, queue: i_structs.SubnetQueue, parent_name=None,
*args):
self.update_queue(queue, parent_name)
def ping(self, host, count=10) -> Optional[Tuple[int, int]]:
r = self._exec_cmd((
@ -454,7 +429,8 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
return
interface = r['!re'].get('=interface')
r = self._exec_cmd((
'/ping', '=address=%s' % host, '=arp-ping=yes', '=interval=100ms', '=count=%d' % count,
'/ping', '=address=%s' % host, '=arp-ping=yes', '=interval=100ms',
'=count=%d' % count,
'=interface=%s' % interface
))
res = r.get('!re')
@ -462,51 +438,83 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs
received, sent = int(res.get('=received')), int(res.get('=sent'))
return received, sent
def add_tariff_range(self, tariff_list: VectorTariff):
pass
def read_users(self) -> i_structs.VectorQueue:
return self.read_queue_iter()
def remove_tariff_range(self, tariff_list: VectorTariff):
pass
@staticmethod
def _build_db_queues(users_from_db: Iterator[Any]) -> Generator:
# Корневая очередь
# FIXME: Корневую очередь надо брать откуда-то
root_queue = i_structs.SubnetQueue(
name='queue-root',
network='10.0.0.0/8',
max_limit=2048,
queue_type=i_structs.SubnetQueue.QUEUE_ROOT
)
def add_tariff(self, tariff: TariffStruct):
pass
# выберем структуры подсетей
db_subnet_queues = (i_structs.SubnetQueue(
name="net_%s" % db_net.network,
network=db_net.get_network(),
max_limit=float(db_net.speed),
queue_type=i_structs.SubnetQueue.QUEUE_SUBNET
) for db_net in NetworkModel.objects.all().iterator())
queues_struct_gen = (
ab.build_agent_struct() for ab in users_from_db
if ab is not None and ab.is_access()
)
def update_tariff(self, tariff: TariffStruct):
pass
r = [q for q in queues_struct_gen if q is not None]
r.insert(0, root_queue)
r.extend(db_subnet_queues)
return r, root_queue
def remove_tariff(self, tid: int):
pass
def _queues_diff(self, users_from_db: Iterator):
queues_from_db, root_queue = self._build_db_queues(users_from_db)
queues_from_gw = tuple(self.read_queue_iter())
def read_users(self) -> VectorAbon:
all_ips = set(ip for ip, mkid in self.read_ips_iter(LIST_USERS_ALLOWED))
queues = (q for q in self.read_queue_iter() if all_ips.issuperset(q.ips))
return queues
# TODO: надо чтоб корневая очередь тоже создавалась
def lease_free(self, user: AbonStruct, lease):
queue = self.find_queue('uid%d' % user.uid)
if queue is None:
return
if len(queue.ips) > 1:
if queue is not None:
user.ips = tuple(i for i in user.ips if i != lease)
self.update_queue(user, queue)
ip = self.find_ip(lease, LIST_USERS_ALLOWED)
if ip is not None:
self.remove_ip(ip.get('=.id'))
else:
raise NasFailedResult(_('You cannot disable last session'))
db_queues_subnets = (
q for q in queues_from_db
if q.queue_type == i_structs.SubnetQueue.QUEUE_SUBNET
)
gw_queues_subnets = tuple(
q for q in queues_from_gw
if q.queue_type == i_structs.SubnetQueue.QUEUE_SUBNET
)
subnets_for_add, subnets_for_del = core.diff_set(
set(db_queues_subnets), set(gw_queues_subnets))
def lease_start(self, user: AbonStruct, lease):
if not issubclass(lease.__class__, _BaseAddress):
lease = ip_address(lease)
if not isinstance(user, AbonStruct):
raise TypeError
ip = self.find_ip(lease, LIST_USERS_ALLOWED)
if ip is None:
self.add_ip(LIST_USERS_ALLOWED, lease)
queue = self.find_queue('uid%d' % user.uid)
user.ips += lease,
if queue is None:
self.add_queue(user)
else:
self.update_queue(user, queue)
self.remove_queue_range(
(q.queue_id for q in subnets_for_del)
)
for q in subnets_for_add:
self.add_queue(q, parent_name=root_queue.name)
del subnets_for_add, subnets_for_del
db_queue_users = (
q for q in queues_from_db
if q.queue_type == i_structs.SubnetQueue.QUEUE_LEAF
)
gw_queue_users = (
q for q in queues_from_gw
if q.queue_type == i_structs.SubnetQueue.QUEUE_LEAF
)
user_q_for_add, user_q_for_del = core.diff_set(set(db_queue_users),
set(gw_queue_users))
self.remove_queue_range(
(q.queue_id for q in user_q_for_del)
)
for q in user_q_for_add:
find_filter = filter(
lambda qe: qe.network.overlaps(q.network),
gw_queues_subnets
)
parent_subnet = next(find_filter, root_queue)
self.add_queue(q, parent_name=parent_subnet.name)
def sync_nas(self, users_from_db: Iterator):
self._queues_diff(users_from_db)

110
nas_app/nas_managers/structs.py

@ -1,5 +1,5 @@
from abc import ABCMeta
from ipaddress import ip_address
from ipaddress import ip_network, _BaseNetwork
from typing import Iterable
@ -7,68 +7,76 @@ class BaseStruct(object, metaclass=ABCMeta):
__slots__ = ()
# Как обслуживается абонент
class TariffStruct(BaseStruct):
__slots__ = ('tid', 'speedIn', 'speedOut')
class SubnetQueue(BaseStruct):
__slots__ = ('name', '_net', '_max_limit', '_queue_type',
'is_access', 'queue_id')
def __init__(self, tariff_id=0, speed_in=None, speed_out=None):
self.tid = int(tariff_id)
self.speedIn = speed_in or 0
self.speedOut = speed_out or 0
# Queue types
QUEUE_UNKNOWN = 0
QUEUE_ROOT = 1
QUEUE_SUBNET = 2
QUEUE_LEAF = 3
# Yes, if all variables is zeroed
def is_empty(self):
return self.tid == 0 and self.speedIn == 0 and self.speedOut == 0
def __eq__(self, other):
# не сравниваем id, т.к. тарифы с одинаковыми скоростями для NAS одинаковы
# Да и иногда не удобно доставать из nas id тарифы из базы
return self.speedIn == other.speedIn and self.speedOut == other.speedOut
def __str__(self):
return "Id=%d, speedIn=%.2f, speedOut=%.2f" % (self.tid, self.speedIn, self.speedOut)
# нужно чтоб хеши тарифов In10,Out20 и In20,Out10 были разными
# поэтому сначала float->str и потом хеш
def __hash__(self):
return hash(str(self.speedIn) + str(self.speedOut))
def __init__(self, name: str, network, max_limit=0.0,
queue_type=QUEUE_UNKNOWN, is_access=True, queue_id=None):
super().__init__()
self.name = name
self.network = network
self.max_limit = max_limit
self.queue_type = queue_type
self.is_access = is_access
self.queue_id = queue_id
def get_max_limit(self):
return self._max_limit
def set_max_limit(self, v):
if isinstance(v, tuple):
self._max_limit = v
elif isinstance(v, str):
s_in, s_out = v.split('/')
self._max_limit = float(s_in), float(s_out)
elif isinstance(v, (int, float)):
sp = float(v)
self._max_limit = sp, sp
else:
raise ValueError('Unexpected format for max_limit')
max_limit = property(get_max_limit, set_max_limit)
# Abon from database
class AbonStruct(BaseStruct):
__slots__ = ('uid', '_ips', 'tariff', 'is_access', 'queue_id')
def get_network(self):
return self._net
def __init__(self, uid=0, ips=None, tariff=None, is_access=True):
self.uid = int(uid or 0)
if ips is None:
self._ips = ()
def set_network(self, v):
if isinstance(v, (str, int)):
self._net = ip_network(v, strict=False)
elif issubclass(v.__class__, _BaseNetwork):
self._net = v
else:
self._ips = tuple(ip_address(ip) for ip in ips)
self.tariff = tariff
self.is_access = is_access
self.queue_id = 0
raise ValueError('Unexpected format for network')
def get_ips(self):
return self._ips
network = property(get_network, set_network)
def set_ips(self, v):
self._ips = set(v)
def get_queue_type(self):
return self._queue_type
ips = property(get_ips, set_ips, doc='Ip addresses')
def set_queue_type(self, v):
if not isinstance(v, int):
raise ValueError('queue_type must be int')
if v < self.QUEUE_UNKNOWN or v > self.QUEUE_LEAF:
raise IndexError('queue_type out of range')
self._queue_type = v
def __eq__(self, other):
if not isinstance(other, AbonStruct):
raise TypeError
r = self.uid == other.uid and self._ips == other._ips
r = r and self.tariff == other.tariff
return r
queue_type = property(get_queue_type, set_queue_type)
def __str__(self):
return "uid=%d, ips=[%s], tariff=%s" % (self.uid, ';'.join(str(i) for i in self._ips), self.tariff or '<No Service>')
def __eq__(self, other):
return self.network == other.network and self.max_limit == other.max_limit
def __hash__(self):
return hash(hash(self._ips) + hash(self.tariff) if self.tariff is not None else 0)
return hash(str(self.max_limit) + str(self.network))
def __repr__(self):
return "net %s" % self.network
VectorAbon = Iterable[AbonStruct]
VectorTariff = Iterable[TariffStruct]
VectorQueue = Iterable[SubnetQueue]

4
nas_app/views.py

@ -34,8 +34,8 @@ class NasCreateView(CreateView):
def form_valid(self, form):
r = super(NasCreateView, self).form_valid(form)
assign_perm("nas_app.change_nasmodel", self.request.user, self.object)
assign_perm("nas_app.view_nas", self.request.user, self.object)
assign_perm("nas_app.delete_nas", self.request.user, self.object)
assign_perm("nas_app.view_nasmodel", self.request.user, self.object)
assign_perm("nas_app.delete_nasmodel", self.request.user, self.object)
self.request.user.log(self.request.META, 'cnas', '"%(title)s", %(ip)s, %(type)s' % {
'title': self.object.title,
'ip': self.object.ip_address,

10
periodic.py

@ -9,7 +9,6 @@ from django.utils import timezone
from django.db import transaction
from django.db.models import signals, Count
from abonapp.models import Abon, AbonTariff, abontariff_pre_delete, PeriodicPayForId, AbonLog
from ip_pool.models import IpLeaseModel
from nas_app.nas_managers import NasNetworkError, NasFailedResult
from nas_app.models import NASModel
from djing.lib import LogicError
@ -24,10 +23,8 @@ class NasSyncThread(Thread):
try:
tm = self.nas.get_nas_manager()
users = Abon.objects \
.annotate(ips_count=Count('ip_addresses')) \
.filter(is_active=True, ips_count__gt=0, nas=self.nas) \
.exclude(current_tariff=None) \
.prefetch_related('ip_addresses') \
.filter(is_active=True, nas=self.nas) \
.exclude(current_tariff=None, ip_address=None) \
.iterator()
tm.sync_nas(users)
except NasNetworkError as er:
@ -102,9 +99,6 @@ def main():
for pay in ppays:
pay.payment_for_service(now=now)
# Remove old inactive ip leases
IpLeaseModel.objects.expired().filter(is_active=False).delete()
# sync subscribers on NAS
threads = tuple(NasSyncThread(nas) for nas in NASModel.objects.
annotate(usercount=Count('abon')).

2
searchapp/views.py

@ -21,7 +21,7 @@ def home(request):
if s:
if re.match(IP_ADDR_REGEX, s):
abons = Abon.objects.filter(ip_addresses__ip=s)
abons = Abon.objects.filter(ip_address=s)
devices = Device.objects.filter(ip_address=s)
else:
abons = Abon.objects.filter(

12
systemd_units/djing_backup.service

@ -0,0 +1,12 @@
[Unit]
Description=Backup for djing
[Service]
Type=simple
ExecStart=/var/backups/do_backup.sh
WorkingDirectory=/var/backups
User=root
Group=root
[Install]
WantedBy=multi-user.target

11
systemd_units/djing_backup.timer

@ -0,0 +1,11 @@
[Unit]
Description=Run backup periodically
[Timer]
OnCalendar=*-*-* 8,12,14,16,19,23:15:0
Persistent=true
Unit=djing_backup.service
[Install]
WantedBy=timers.target

8
systemd_units/do_backup.sh

@ -1,16 +1,14 @@
#!/bin/bash
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
PATH=/usr/bin:/usr/sbin:/bin
cd /var/backups
file="djing`date "+%Y-%m-%d_%H.%M.%S"`.sql.gz"
mysql_passw=MYSQL ROOT PASSWORD
export PGPASSWORD=POSTGRES ROOT PASSWORD
echo show tables | mysql -uroot -p$mysql_passw djingdb | \
grep -v '^flowstat' | grep -v 'traflost' | grep -v '^Tables' | \
xargs mysqldump -R -Q --add-locks -uroot --password=$mysql_passw djingdb $1 | gzip -9 > $file
pg_dump -O -d djing -h localhost -U djing | gzip > $file
chmod 400 $file
./webdav_backup.py $file

Loading…
Cancel
Save