Browse Source

Fix bugs in nas in subscribers management

devel
bashmak 8 years ago
parent
commit
6b7abe9f0a
  1. 18
      abonapp/forms.py
  2. 44
      abonapp/models.py
  3. 20
      abonapp/templates/abonapp/editAbon.html
  4. 11
      abonapp/templates/abonapp/peoples.html
  5. 2
      abonapp/urls.py
  6. 55
      abonapp/views.py
  7. 33
      agent/commands/dhcp.py
  8. 2
      agent/core.py
  9. 129
      agent/mod_mikrotik.py
  10. 17
      agent/structs.py
  11. 5
      djing/settings.py
  12. 2
      ip_pool/admin.py
  13. 84
      ip_pool/fields.py
  14. 30
      ip_pool/forms.py
  15. 96
      ip_pool/models.py
  16. 11
      ip_pool/templates/ip_pool/ip_leases_list.html
  17. 6
      ip_pool/templates/ip_pool/net_add.html
  18. 8
      ip_pool/templates/ip_pool/net_edit.html
  19. 4
      ip_pool/templates/ip_pool/network_list.html
  20. 2
      ip_pool/urls.py
  21. 5
      periodic.py
  22. 16
      static/js/cidr.js

18
abonapp/forms.py

@ -5,7 +5,6 @@ from random import choice
from string import digits, ascii_lowercase
from . import models
from django.conf import settings
from djing import IP_ADDR_REGEX
def generate_random_chars(length=6, chars=digits, split=2, delimiter=''):
@ -42,8 +41,8 @@ class AbonForm(forms.ModelForm):
abon_group_queryset = None
if abon_group_queryset is not None:
self.fields['street'].queryset = abon_group_queryset
if instance is not None and instance.is_dynamic_ip:
self.fields['ip_address'].widget.attrs['readonly'] = True
#if instance is not None and instance.is_dynamic_ip:
# self.fields['ip_address'].widget.attrs['readonly'] = True
username = forms.CharField(max_length=127, required=False, initial=generate_random_username,
widget=forms.TextInput(attrs={
@ -56,13 +55,13 @@ class AbonForm(forms.ModelForm):
'type': 'password', 'autocomplete': 'new-password'
}), label=_('Password'))
ip_address = forms.CharField(widget=forms.TextInput(attrs={
'pattern': IP_ADDR_REGEX
}), label=_('Ip Address'), required=False)
# ip_address = forms.CharField(widget=forms.TextInput(attrs={
# 'pattern': IP_ADDR_REGEX
# }), label=_('Ip Address'), required=False)
class Meta:
model = models.Abon
fields = ('username', 'telephone', 'fio', 'group', 'description', 'street', 'house', 'is_active', 'ip_address')
fields = ('username', 'telephone', 'fio', 'group', 'description', 'street', 'house', 'is_active')
widgets = {
'fio': forms.TextInput(attrs={
'placeholder': _('fio'),
@ -72,8 +71,7 @@ class AbonForm(forms.ModelForm):
'placeholder': _('telephone placeholder'),
'pattern': getattr(settings, 'TELEPHONE_REGEXP', r'^(\+[7,8,9,3]\d{10,11})?$')
}),
'description': forms.Textarea(attrs={'rows': '4'}),
'is_active': forms.NullBooleanSelect(attrs={'class': 'form-control'})
'description': forms.Textarea(attrs={'rows': '4'})
}
def save(self, commit=True):
@ -141,7 +139,7 @@ class ExportUsersForm(forms.Form):
FIELDS_CHOICES = (
('username', _('profile username')),
('fio', _('fio')),
('ip_address', _('Ip Address')),
#('ip_address', _('Ip Address')),
('description', _('Comment')),
('street__name', _('Street')),
('house', _('House')),

44
abonapp/models.py

@ -1,9 +1,9 @@
from datetime import datetime
from ipaddress import ip_address
from typing import Optional
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models, connection, transaction
from django.db.models.signals import post_delete, pre_delete, post_init
@ -15,7 +15,8 @@ from django.utils.translation import ugettext_lazy as _, gettext
from accounts_app.models import UserProfile, MyUserManager, BaseAccount
from agent import Transmitter, AbonStruct, TariffStruct, NasFailedResult, NasNetworkError
from group_app.models import Group
from djing.lib import ip2int, LogicError
from djing.lib import LogicError
from ip_pool.models import IpLeaseModel
from tariff_app.models import Tariff, PeriodicPay
from bitfield import BitField
@ -92,7 +93,8 @@ class Abon(BaseAccount):
current_tariff = models.ForeignKey(AbonTariff, null=True, blank=True, on_delete=models.SET_NULL)
group = models.ForeignKey(Group, models.SET_NULL, blank=True, null=True, verbose_name=_('User group'))
ballance = models.FloatField(default=0.0)
ip_address = models.GenericIPAddressField(blank=True, null=True, verbose_name=_('Ip Address'))
ip_addresses = models.ManyToManyField(IpLeaseModel, verbose_name=_('Ip addresses'))
# ip_address = models.GenericIPAddressField(blank=True, null=True, verbose_name=_('Ip Address'))
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)
@ -213,23 +215,24 @@ class Abon(BaseAccount):
# make subscriber from agent structure
def build_agent_struct(self):
if self.ip_address:
user_ip = ip2int(self.ip_address)
else:
abon_addresses = tuple(ip_address(i.ip) for i in self.ip_addresses.filter(is_active=True))
if not abon_addresses:
return
abon_tariff = self.active_tariff()
if abon_tariff is None:
return
trf = abon_tariff.tariff
agent_trf = TariffStruct(trf.id, trf.speedIn, trf.speedOut)
return AbonStruct(self.pk, user_ip, agent_trf, bool(self.is_active))
def clean(self):
# check if ip address already busy
if self.ip_address is not None and Abon.objects.filter(ip_address=self.ip_address).exclude(
pk=self.pk).count() > 0:
raise ValidationError({'ip_address': (gettext('Ip address already exist'),)})
return super(Abon, self).clean()
agent_trf = None
else:
trf = abon_tariff.tariff
agent_trf = TariffStruct(trf.id, trf.speedIn, trf.speedOut)
return AbonStruct(self.pk, abon_addresses, agent_trf, self.is_access())
# def clean(self):
# # check if ip address already busy
# abon_addresses = tuple(p.ip for p in self.ip_addresses.all().iterator())
# if self.ip_address is not None and Abon.objects.filter(ip_address=self.ip_address).exclude(
# pk=self.pk).count() > 0:
# raise ValidationError({'ip_address': (gettext('Ip address already exist'),)})
# return super(Abon, self).clean()
def sync_with_nas(self, created: bool) -> Optional[Exception]:
agent_abon = self.build_agent_struct()
@ -245,6 +248,13 @@ class Abon(BaseAccount):
print('ERROR:', e)
return e
# def disable_on_nas(self):
# agent_abon = self.build_agent_struct()
# if agent_abon is None:
# return
# tm = Transmitter()
# tm.remove_user(agent_abon)
def get_absolute_url(self):
return resolve_url('abonapp:abon_home', self.group.id, self.username)

20
abonapp/templates/abonapp/editAbon.html

@ -37,17 +37,17 @@
{% bootstrap_field form.telephone form_group_class='form-group-sm' addon_after_class='input-group-btn' addon_after=bt %}
{% endwith %}
{% bootstrap_field form.is_active form_group_class='form-group-sm' %}
{# Ip address field #}
{% trans 'Reset ip' as tx %}
{% url 'abonapp:reset_ip' group.pk abon.username as url %}
{% bootstrap_button '' button_type='link' icon='refresh' button_class='btn-default btn-cmd' id='iprefreshbtn' href=url size='sm' title=tx as bt %}
{% bootstrap_field form.ip_address form_group_class='form-group-sm' addon_after_class='input-group-btn' addon_after=bt %}
{# {% trans 'Reset ip' as tx %}#}
{# {% url 'abonapp:reset_ip' group.pk abon.username as url %}#}
{# {% bootstrap_button '' button_type='link' icon='refresh' button_class='btn-default btn-cmd' id='iprefreshbtn' href=url size='sm' title=tx as bt %}#}
{# {% bootstrap_field form.ip_address form_group_class='form-group-sm' addon_after_class='input-group-btn' addon_after=bt %}#}
{% bootstrap_field form.street form_group_class='form-group-sm' %}
{% bootstrap_field form.house form_group_class='form-group-sm' %}
{% bootstrap_field form.is_active form_group_class='form-group-sm' %}
{% bootstrap_field form.group form_group_class='form-group-sm' %}
@ -203,9 +203,13 @@
{% for lease in abon.ip_addresses.all %}
<li class="list-group-item">
{% if lease.is_active %}
<a href="{% url 'abonapp:user_session_free' group.pk abon.username lease.pk %}" class="btn btn-default btn-sm" title="{% trans 'Free session' %}">&times;</a>
<a href="{% url 'abonapp:user_session_free' group.pk abon.username lease.pk %}" class="btn btn-danger btn-xs" title="{% trans 'Free session' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% else %}
<a href="#" disabled class="btn btn-default btn-sm">&times;</a>
<a href="{% url 'abonapp:user_session_start' group.pk abon.username lease.pk %}" class="btn btn-success btn-xs" title="{% trans 'Start session' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-flash"></span>
</a>
{% endif %}
<span{{ lease.is_active|yesno:', class="text-muted"'|safe }}>
<b>{{ lease }}</b>
@ -217,7 +221,7 @@
{% endfor %}
</ul>
<div class="panel-footer">
<a href="#" class="btn btn-default btn-sm">
<a href="#" class="btn btn-success btn-sm">
<span class="glyphicon glyphicon-plus"></span>
<span class="hidden-xs">{% trans 'Add' %}</span>
</a>

11
abonapp/templates/abonapp/peoples.html

@ -31,13 +31,7 @@
{% if order_by == 'username' %}<span class="glyphicon glyphicon-filter"></span>{% endif %}
</th>
<th class="hidden-xs">{% trans 'Last traffic' %}</th>
<th class="col-xs-1 hidden-md">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_replace request order_by='ip_address' dir=dir|default:'down' %}">
{% trans 'Ip address' %}
</a>
{% if order_by == 'ip_address' %}<span class="glyphicon glyphicon-filter"></span>{% endif %}
</th>
<th class="col-xs-2">
<th class="col-xs-3">
<a href="{% url 'abonapp:people_list' group.pk %}?{% url_replace request order_by='fio' dir=dir|default:'down' %}">
{% trans 'fio' %}
</a>
@ -93,7 +87,6 @@
{% endif %}
{% endif %}
</td>
<td class="col-xs-1 hidden-md">{{ human.ip_address|default:_('Not assigned') }}</td>
<td class="col-xs-2">{{ human.fio }}</td>
<td class="col-xs-2">{{ human.street|default:_('Not assigned') }}</td>
<td class="col-xs-1">{{ human.house|default:'-' }}</td>
@ -123,7 +116,7 @@
</tr>
{% empty %}
<tr>
<td colspan="12">
<td colspan="11">
{% trans 'Subscribers not found' %}.
{% if perms.abonapp.add_abon %}
<a href="{% url 'abonapp:add_abon' group.pk %}">{% trans 'Add abon' %}</a>

2
abonapp/urls.py

@ -26,6 +26,8 @@ subscriber_patterns = [
url(r'^tel/add/$', views.tel_add, name='telephone_new'),
url(r'^tel/del/$', views.tel_del, name='telephone_del'),
url(r'^markers/$', views.EditSibscriberMarkers.as_view(), name='markers_edit'),
url(r'^session/(?P<lease_id>\d+)/free$', views.user_session_toggle, {'action': 'free'}, name='user_session_free'),
url(r'^session/(?P<lease_id>\d+)/start$', views.user_session_toggle, {'action': 'start'}, name='user_session_start'),
url(r'^periodic_pay$', views.add_edit_periodic_pay, name='add_periodic_pay'),
url(r'^periodic_pay(?P<periodic_pay_id>\d+)/$', views.add_edit_periodic_pay, name='add_periodic_pay'),
url(r'^periodic_pay(?P<periodic_pay_id>\d+)/del/$', views.del_periodic_pay, name='del_periodic_pay')

55
abonapp/views.py

@ -39,6 +39,7 @@ class PeoplesListView(BaseOrderedFilteringList):
template_name = 'abonapp/peoples.html'
def get_queryset(self):
# TODO: optimize that query
street_id = lib.safe_int(self.request.GET.get('street'))
gid = lib.safe_int(self.kwargs.get('gid'))
peoples_list = models.Abon.objects.all().select_related('group', 'street', 'current_tariff')
@ -48,10 +49,11 @@ class PeoplesListView(BaseOrderedFilteringList):
peoples_list = peoples_list.filter(group__pk=gid)
try:
for abon in peoples_list:
if abon.ip_address is not None:
for abon in peoples_list.iterator():
ips = tuple(p.ip for p in abon.ip_addresses.filter(is_active=True))
if len(ips) > 0:
try:
abon.stat_cache = StatCache.objects.get(ip=abon.ip_address)
abon.stat_cache = StatCache.objects.get(ip__in=ips)
except StatCache.DoesNotExist:
pass
except lib.LogicError as e:
@ -438,9 +440,9 @@ def pick_tariff(request, gid, uname):
@permission_required('abonapp.delete_abontariff')
def unsubscribe_service(request, gid, uname, abon_tariff_id):
try:
abon = get_object_or_404(models.Abon, username=uname)
#abon = get_object_or_404(models.Abon, username=uname)
abon_tariff = get_object_or_404(models.AbonTariff, pk=int(abon_tariff_id))
abon.sync_with_nas(created=False)
#abon.disable_on_nas()
abon_tariff.delete()
messages.success(request, _('User has been detached from service'))
except NasFailedResult as e:
@ -938,18 +940,18 @@ def abon_export(request, gid):
}, request=request)
@login_required
@permission_required('abonapp.change_abon')
@permission_required('group_app.can_view_group', (Group, 'pk', 'gid'))
@json_view
def reset_ip(request, gid, uname):
abon = get_object_or_404(models.Abon, username=uname)
abon.ip_address = None
abon.save(update_fields=('ip_address',))
return {
'status': 0,
'dat': "<span class='glyphicon glyphicon-refresh'></span>"
}
# @login_required
# @permission_required('abonapp.change_abon')
# @permission_required('group_app.can_view_group', (Group, 'pk', 'gid'))
# @json_view
# def reset_ip(request, gid, uname):
# abon = get_object_or_404(models.Abon, username=uname)
# abon.ip_address = None
# abon.save(update_fields=('ip_address',))
# return {
# 'status': 0,
# 'dat': "<span class='glyphicon glyphicon-refresh'></span>"
# }
@login_required
@ -1040,6 +1042,23 @@ class EditSibscriberMarkers(UpdateView):
return v
@login_required
@lib.decorators.only_admins
def user_session_toggle(request, gid, uname, lease_id, action=None):
abon = get_object_or_404(models.Abon, username=uname)
lease = abon.ip_addresses.get(pk=lease_id)
if action == 'free':
lease.free()
elif action == 'start':
lease.start()
err = abon.sync_with_nas(created=False)
if err is not None:
messages.error(request, err)
else:
messages.success(request, _('Ip lease has been freed'))
return redirect('abonapp:abon_home', gid, uname)
# API's
@login_required
@lib.decorators.only_admins
@ -1048,7 +1067,7 @@ def abons(request):
ablist = ({
'id': abn.pk,
'tarif_id': abn.active_tariff().tariff.pk if abn.active_tariff() is not None else 0,
'ip': lib.ip2int(abn.ip_address),
'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())

33
agent/commands/dhcp.py

@ -2,6 +2,7 @@ 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]:
@ -16,11 +17,16 @@ def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: i
else:
abon = Abon.objects.get(device=dev)
if not abon.is_dynamic_ip:
print('D:', 'User settings is not dynamic')
return
if abon.ip_address != client_ip:
abon.ip_address = client_ip
abon.save(update_fields=('ip_address',))
return 'User settings is not dynamic'
existed_client_ips = tuple(l.ip for l in abon.ip_addresses.all())
if client_ip not in existed_client_ips:
lease = IpLeaseModel.objects.create_from_ip(
ip=client_ip,
)
if lease is None:
return 'Subnet not found'
abon.ip_addresses.add(lease)
abon.save()
if abon.is_access():
abon.sync_with_nas(created=False)
else:
@ -38,15 +44,18 @@ def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: i
return 'MultipleObjectsReturned:' + ' '.join((type(e), e, str(switch_port)))
def dhcp_expiry(client_ip) -> Optional[str]:
def dhcp_expiry(client_ip: str) -> Optional[str]:
try:
abon = Abon.objects.get(ip_address=client_ip)
abon.ip_address = None
abon.save(update_fields=('ip_address',))
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.sync_with_nas(created=False)
except Abon.DoesNotExist:
return "Subscriber with ip %s does not exist" % client_ip
except IpLeaseModel.DoesNotExist:
pass
def dhcp_release(client_ip) -> Optional[str]:
def dhcp_release(client_ip: str) -> Optional[str]:
return dhcp_expiry(client_ip)

2
agent/core.py

@ -74,7 +74,7 @@ class BaseTransmitter(metaclass=ABCMeta):
"""
@abstractmethod
def read_users(self) -> Iterable[AbonStruct]:
def read_users(self) -> VectorAbon:
pass
def _diff_users(self, users_from_db: Iterator[Any]) -> Tuple[set, set]:

129
agent/mod_mikrotik.py

@ -2,8 +2,9 @@ import re
import socket
import binascii
from hashlib import md5
from ipaddress import ip_network
from typing import Iterable, Optional, Tuple, Generator, Dict
from djing.lib import safe_int
from .structs import TariffStruct, AbonStruct, IpStruct, VectorAbon, VectorTariff
from . import settings as local_settings
from django.conf import settings
@ -240,9 +241,9 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
a = AbonStruct(
uid=int(dat['=name'][3:]),
# FIXME: тут в разных микротиках или =target-addresses или =target
ip=dat['=target'][:-3],
ips=(int(ip_network(ip).network_address) for ip in dat['=target'].split(',')),
tariff=t,
is_active=False if dat['=disabled'] == 'false' else True
is_access=False if dat['=disabled'] == 'false' else True
)
a.queue_id = dat['=.id']
return a
@ -268,24 +269,24 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
'/queue/simple/add',
'=name=uid%d' % user.uid,
# FIXME: тут в разных микротиках или =target-addresses или =target
'=target=%s' % user.ip,
'=target=%s' % ','.join(str(i) for i in user.ips),
'=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn),
'=queue=MikroBILL_SFQ/MikroBILL_SFQ',
'=burst-time=1/1'
))
def remove_queue(self, user: AbonStruct) -> None:
def remove_queue(self, user: AbonStruct, queue: AbonStruct=None) -> None:
if not isinstance(user, AbonStruct):
raise TypeError
q = self.find_queue('uid%d' % user.uid)
if q is not None:
queue_id = safe_int(getattr(q, 'queue_id'))
if queue_id != 0:
r = self._exec_cmd((
if queue is None:
queue = self.find_queue('uid%d' % user.uid)
if queue is not None:
queue_id = getattr(queue, 'queue_id')
if queue_id is not None:
self._exec_cmd((
'/queue/simple/remove',
'=.id=%d' % queue_id
'=.id=%s' % queue_id
))
print(r)
def remove_queue_range(self, q_ids: Iterable[str]):
self._exec_cmd(('/queue/simple/remove', '=numbers=' + ','.join(q_ids)))
@ -299,18 +300,18 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
if queue is None:
return self.add_queue(user)
else:
mk_id = safe_int(getattr(queue, 'queue_id', 0))
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' % user.ip,
'=target=%s' % ','.join(str(i) for i in user.ips),
'=queue=MikroBILL_SFQ/MikroBILL_SFQ',
'=burst-time=1/1'
]
if mk_id != 0:
cmd.insert(1, '=.id=%d' % mk_id)
if mk_id is not None:
cmd.insert(1, '=.id=%s' % mk_id)
r = self._exec_cmd(cmd)
return r
@ -340,13 +341,11 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
'=.id=*' + str(mk_id).replace('*', '')
))
def remove_ip_range(self, items: Iterable[IpAddressListObj]):
ids = tuple(ip.mk_id for ip in items if isinstance(ip, IpAddressListObj))
if len(ids) > 0:
return self._exec_cmd((
'/ip/firewall/address-list/remove',
'=numbers=*%s' % ',*'.join(ids)
))
def remove_ip_range(self, ip_firewall_ids: Iterable[str]):
return self._exec_cmd((
'/ip/firewall/address-list/remove',
'=numbers=%s' % ','.join(ip_firewall_ids)
))
def find_ip(self, ip: IpStruct, list_name: str):
if not isinstance(ip, IpStruct):
@ -380,58 +379,56 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
raise ValueError('*users* is used twice, generator does not fit')
queue_ids = (usr.queue_id for usr in users if usr is not None)
self.remove_queue_range(queue_ids)
for ip in (user.ip for user in users if isinstance(user, AbonStruct)):
ip_list_entity = self.find_ip(ip, LIST_USERS_ALLOWED)
if ip_list_entity:
self.remove_ip(ip_list_entity.get('=.id'))
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 not isinstance(user.ip, IpStruct):
raise TypeError
if user.tariff is None:
return
if not isinstance(user.tariff, TariffStruct):
raise TypeError
try:
self.add_queue(user)
except (NasNetworkError, NasFailedResult) as e:
print('Error:', e)
try:
self.add_ip(LIST_USERS_ALLOWED, user.ip)
except (NasNetworkError, NasFailedResult) as e:
print('Error:', e)
self.add_queue(user)
for ip in user.ips:
if not isinstance(ip, IpStruct):
raise TypeError
self.add_ip(LIST_USERS_ALLOWED, ip)
def remove_user(self, user: AbonStruct):
self.remove_queue(user)
firewall_ip_list_obj = self.find_ip(user.ip, LIST_USERS_ALLOWED)
if firewall_ip_list_obj:
self.remove_ip(firewall_ip_list_obj.get('=.id'))
firewall_ip_list_ids = (self.find_ip(ip, LIST_USERS_ALLOWED).get('=.id') for ip in user.ips)
self.remove_ip_range(firewall_ip_list_ids)
def update_user(self, user: AbonStruct, *args):
if not isinstance(user.ip, IpStruct):
raise TypeError
find_res = self.find_ip(user.ip, LIST_USERS_ALLOWED)
# queue is instance of AbonStruct
queue = self.find_queue('uid%d' % user.uid)
if not user.is_active:
# если не активен - то и обновлять не надо
# но и выключить на всяк случай надо, а то вдруг был включён
if find_res:
# и если найден был - то удалим ip из разрешённых
self.remove_ip(find_res.get('=.id'))
if queue is not None:
self.remove_queue(user)
return
for ip in user.ips:
if not isinstance(ip, IpStruct):
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)
self.remove_queue(user, queue)
return
if not user.is_access:
return
# если не найден
if find_res is None:
# добавим запись об абоненте
self.add_ip(LIST_USERS_ALLOWED, user.ip)
# Проверяем шейпер
if queue is None:
@ -470,15 +467,15 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
def remove_tariff(self, tid: int):
pass
def read_users(self) -> Iterable[AbonStruct]:
def read_users(self) -> VectorAbon:
# shapes is ShapeItem
allowed_ips = set(self.read_ips_iter(LIST_USERS_ALLOWED))
queues = tuple(q for q in self.read_queue_iter() if q.ip in allowed_ips)
ips_from_queues = set((q.ip, q) for q in queues)
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)
# delete ip addresses that are in firewall/address-list and there are no corresponding in queues
diff = tuple(allowed_ips - ips_from_queues)
if len(diff) > 0:
self.remove_ip_range(diff)
#diff = tuple(allowed_ips - ips_from_queues)
#if len(diff) > 0:
# self.remove_ip_range(diff)
return queues

17
agent/structs.py

@ -64,27 +64,30 @@ class TariffStruct(BaseStruct):
# Абонент из базы
class AbonStruct(BaseStruct):
__slots__ = ('uid', 'ip', 'tariff', 'is_active', 'queue_id')
__slots__ = ('uid', 'ips', 'tariff', 'is_access', 'queue_id')
def __init__(self, uid=0, ip=None, tariff=None, is_active=True):
def __init__(self, uid=0, ips=None, tariff=None, is_access=True):
self.uid = int(uid or 0)
self.ip = IpStruct(ip)
if ips is None:
self.ips = ()
else:
self.ips = tuple(IpStruct(ip) for ip in ips)
self.tariff = tariff
self.is_active = is_active
self.is_access = is_access
self.queue_id = 0
def __eq__(self, other):
if not isinstance(other, AbonStruct):
raise TypeError
r = self.uid == other.uid and self.ip == other.ip
r = self.uid == other.uid and self.ips == other.ips
r = r and self.tariff == other.tariff
return r
def __str__(self):
return "uid=%d, ip=%s, tariff=%s" % (self.uid, self.ip, self.tariff or '<No Service>')
return "uid=%d, ips=[%s], tariff=%s" % (self.uid, ';'.join(self.ips), self.tariff or '<No Service>')
def __hash__(self):
return hash(int(self.ip) + hash(self.tariff)) if self.tariff is not None else 0
return hash(hash(self.ips) + hash(self.tariff)) if self.tariff is not None else 0
# Правило шейпинга в фаере, или ещё можно сказать услуга абонента на NAS

5
djing/settings.py

@ -212,3 +212,8 @@ BOOTSTRAP3 = {
# Field class to use in horizontal forms
'horizontal_field_class': 'col-md-9',
}
# 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.
LEASE_LIVE_TIME = 86400

2
ip_pool/admin.py

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

84
ip_pool/fields.py

@ -0,0 +1,84 @@
from ipaddress import ip_network
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.db import models
from django.forms.fields import CharField
def validate_ipv46_address_with_subnet(v: str):
try:
ip = ip_network(v)
return str(ip)
except ValueError as e:
raise ValidationError(e)
class GenericIPAddressFormField(CharField):
def __init__(self, unpack_ipv4=False, *args, **kwargs):
self.unpack_ipv4 = unpack_ipv4
self.default_validators = validate_ipv46_address_with_subnet,
del kwargs['protocol']
super(GenericIPAddressFormField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value in self.empty_values:
return ''
value = value.strip()
if value and ':' in value:
ip = ip_network(value, strict=False)
return ip.compressed
return value
class GenericIpAddressWithPrefix(models.GenericIPAddressField):
description = _("IP address with prefix length, or subnet for ipv4")
def __init__(self, prefix=None, *args, **kwargs):
self.prefix = prefix
super(GenericIpAddressWithPrefix, self).__init__(*args, **kwargs)
self.default_error_messages['invalid'] = _('Enter a valid IPv4 or IPv6 address with prefix length.')
self.max_length = 43
def deconstruct(self):
name, path, args, kwargs = super(GenericIpAddressWithPrefix, self).deconstruct()
if kwargs.get("max_length") == 43:
del kwargs['max_length']
return name, path, args, kwargs
@property
def validators(self):
return validate_ipv46_address_with_subnet,
def to_python(self, value):
if value is None:
return None
value = value.strip()
if ':' in value:
ip = ip_network(value)
return ip.compressed
return value
def get_db_prep_value(self, value, connection, prepared=False):
if not prepared:
value = self.get_prep_value(value)
return connection.ops.adapt_ipaddressfield_value(value)
def get_prep_value(self, value):
value = super(GenericIpAddressWithPrefix, self).get_prep_value(value)
if value is None:
return None
if value and ':' in value:
try:
return ip_network(value)
except ValidationError:
pass
return value
def formfield(self, **kwargs):
defaults = {
'protocol': self.protocol,
'form_class': GenericIPAddressFormField,
}
defaults.update(kwargs)
return super(GenericIpAddressWithPrefix, self).formfield(**defaults)

30
ip_pool/forms.py

@ -1,4 +1,5 @@
from netaddr import IPNetwork, AddrFormatError
from ipaddress import ip_network
from django import forms
from django.core.exceptions import ValidationError
@ -9,35 +10,14 @@ class NetworkForm(forms.ModelForm):
def clean_network(self):
netw = self.data.get('network')
mask = self.data.get('mask')
if netw is None:
return
try:
if mask:
net = IPNetwork('%s/%s' % (netw, mask))
else:
net = IPNetwork(netw)
return str(net.ip)
except AddrFormatError as e:
net = ip_network(netw)
return net.compressed
except ValueError as e:
raise ValidationError(e, code='invalid')
class Meta:
model = models.NetworkModel
fields = '__all__'
widgets = {
'mask': forms.TextInput(attrs={
'pattern': '^\d{1,3}$'
})
}
class EmployedIpForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is None:
self.fields['ip'].initial = '127.0.0.1'
class Meta:
model = models.EmployedIpModel
fields = '__all__'

96
ip_pool/models.py

@ -1,34 +1,25 @@
from typing import Optional
from datetime import timedelta
from ipaddress import ip_network, ip_address
from django.conf import settings
from django.shortcuts import resolve_url
from netaddr import IPNetwork, IPAddress
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djing.fields import MACAddressField
from ip_pool.fields import GenericIpAddressWithPrefix
class NetworkModel(models.Model):
_netw_cache = None
network = models.GenericIPAddressField(
network = GenericIpAddressWithPrefix(
verbose_name=_('IP network'),
help_text=_('Ip address of network. For example: 192.168.1.0 or fde8:6789:1234:1::'),
unique=True
)
mask = models.PositiveSmallIntegerField(
_('Mask'),
help_text=_('Net mask bits length for ipv4 or prefix length for ipv6'),
default=24,
)
work_range_start_ip = models.GenericIPAddressField(
verbose_name=_('Work range start ip'),
help_text=_('For example 192.168.1.2, this is first ip that may be used')
)
work_range_end_ip = models.GenericIPAddressField(
verbose_name=_('Work range end ip'),
help_text=_('Ip may be used until 192.168.1.254')
)
NETWORK_KINDS = (
('inet', _('Internet')),
('guest', _('Guest')),
@ -37,17 +28,14 @@ class NetworkModel(models.Model):
('admin', _('Admin'))
)
kind = models.CharField(_('Kind of network'), max_length=6, choices=NETWORK_KINDS, default='guest')
description = models.CharField(_('Description'), max_length=64)
def __str__(self):
return "%s: %s/%d" % (self.description, self.network, self.mask)
return "%s: %s" % (self.description, self.network)
def get_network(self) -> IPNetwork:
def get_network(self):
if self._netw_cache is None:
self._netw_cache = IPNetwork(self.network)
if self.mask:
self._netw_cache.prefixlen = self.mask
self._netw_cache = ip_network(self.network)
return self._netw_cache
def get_absolute_url(self):
@ -60,28 +48,61 @@ class NetworkModel(models.Model):
ordering = ('description',)
class EmployedIpManager(models.Manager):
class IpLeaseManager(models.Manager):
def get_free_ip(self, network: NetworkModel) -> Optional[IPAddress]:
netw = IPNetwork(network)
def get_free_ip(self, network: NetworkModel):
netw = ip_network(network)
employed_ip_queryset = self.filter(network=network)
free_ip = next(IPAddress(net) for ip, net in zip(
free_ip = next(ip_address(net) for ip, net in zip(
employed_ip_queryset, netw
) if ip != net)
return free_ip
class EmployedIpModel(models.Model):
def create_from_ip(self, ip: str, cidr_subnet: int):
# FIXME: get subnet
raise NotImplementedError
net = ip_network((ip, cidr_subnet), strict=False)
netw_instance = NetworkModel.objects.filter(network=str(net)).first()
if netw_instance is not None:
return self.create(
ip=ip,
network=netw_instance,
is_dynamic=True,
is_active=True
)
def expired(self):
lease_live_time = getattr(settings, 'LEASE_LIVE_TIME')
if lease_live_time is None:
raise ImproperlyConfigured('You must specify LEASE_LIVE_TIME in settings')
senility = now() - timedelta(seconds=lease_live_time)
return self.filter(lease_time__lt=senility, is_active=False)
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'))
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)
objects = EmployedIpManager()
objects = IpLeaseManager()
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 = IPAddress(self.ip)
ip = ip_address(self.ip)
network = self.network.get_network()
if ip not in network:
@ -90,14 +111,15 @@ class EmployedIpModel(models.Model):
'net': network
}, code='invalid')
start_allowed_ip = IPAddress(self.network.work_range_start_ip)
end_allowed_ip = IPAddress(self.network.work_range_end_ip)
if not start_allowed_ip <= ip <= end_allowed_ip:
raise ValidationError(_('Ip address that you entered is not in work range'), code='invalid')
class Meta:
db_table = 'ip_pool_employed_ip'
verbose_name = _('Employed ip')
verbose_name_plural = _('Employed ip addresses')
ordering = ('-id',)
unique_together = ('ip', 'network')
# 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)

11
ip_pool/templates/ip_pool/employed_ip_list.html → ip_pool/templates/ip_pool/ip_leases_list.html

@ -1,17 +1,16 @@
{% extends 'base.html' %}
{% load i18n %}
{% load bootstrap3 %}
{% 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 list' %}</li>
<li class="active">{% trans 'Ip leases list' %}</li>
</ol>
{% endblock %}
{% block page-header %}{% trans 'Ip list' %}{% endblock %}
{% block page-header %}{% trans 'Ip leases list' %}{% endblock %}
{% block main %}
<div class="table-responsive">
@ -19,18 +18,22 @@
<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.network }}</td>
<td><input type="checkbox" {{ ip.is_dynamic|yesno:'checked,' }}></td>
</tr>
{% empty %}
<tr>
<td colspan="2">{% trans 'You have not any available dedicated ips in this network' %}</td>
<td colspan="4">{% trans 'You have not any available dedicated ips in this network' %}</td>
</tr>
{% endfor %}
</tbody>

6
ip_pool/templates/ip_pool/net_add.html

@ -3,9 +3,9 @@
{% load bootstrap3 %}
{% load globaltags %}
{% block additional_link %}
<script src="/static/js/cidr.js"></script>
{% endblock %}
{#{% block additional_link %}#}
{# <script src="/static/js/cidr.js"></script>#}
{#{% endblock %}#}
{% block breadcrumb %}
<ol class="breadcrumb">

8
ip_pool/templates/ip_pool/net_edit.html

@ -3,9 +3,9 @@
{% load bootstrap3 %}
{% load globaltags %}
{% block additional_link %}
<script src="/static/js/cidr.js"></script>
{% endblock %}
{#{% block additional_link %}#}
{# <script src="/static/js/cidr.js"></script>#}
{#{% endblock %}#}
{% block breadcrumb %}
<ol class="breadcrumb">
@ -34,7 +34,7 @@
<a href="{% back_url request %}" class="btn btn-default">
<span class="glyphicon glyphicon-backward"></span> {% trans 'Back' %}
</a>
<a href="{% url 'ip_pool:ip_list' object.pk %}" class="btn btn-default">
<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>

4
ip_pool/templates/ip_pool/network_list.html

@ -27,7 +27,7 @@
{% with can_ch_net=perms.ip_pool.change_networkmodel %}
{% for netw in networks_list %}
<tr>
<td><a href="{% url 'ip_pool:ip_list' netw.id %}">{{ netw }}</a></td>
<td><a href="{% url 'ip_pool:ip_leases_list' netw.id %}">{{ netw }}</a></td>
<td>{{ netw.work_range_start_ip }}</td>
<td>{{ netw.work_range_end_ip }}</td>
<td class="btn-group btn-group-sm btn-group-justified">
@ -42,7 +42,7 @@
<span class="hidden-xs hidden-sm">{% trans 'Edit' %}</span>
</a>
{% endif %}
<a href="{% url 'ip_pool:ip_list' netw.pk %}" class="btn btn-default">
<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>

2
ip_pool/urls.py

@ -7,7 +7,7 @@ app_name = 'ip_pool'
urlpatterns = [
url('^$', views.NetworksListView.as_view(), name='networks'),
url('^network_add/$', views.NetworkCreateView.as_view(), name='net_add'),
url('^(?P<net_id>\d{1,6})/$', views.IpEmployedListView.as_view(), name='ip_list'),
url('^(?P<net_id>\d{1,6})/$', views.IpLeasesListView.as_view(), name='ip_leases_list'),
url('^(?P<net_id>\d{1,6})/edit$', views.NetworkUpdateView.as_view(), name='net_edit'),
]

5
periodic.py

@ -8,6 +8,7 @@ from django.utils import timezone
from django.db import transaction
from django.db.models import signals
from abonapp.models import Abon, AbonTariff, abontariff_pre_delete, PeriodicPayForId, AbonLog
from ip_pool.models import IpLeaseModel
from agent import Transmitter, NasNetworkError, NasFailedResult
from djing.lib import LogicError
@ -49,6 +50,10 @@ def main():
for pay in ppays:
pay.payment_for_service(now=now)
# Remove old inactive ip leases
old_leases = IpLeaseModel.objects.expired()
old_leases.delete()
if __name__ == "__main__":
try:

16
static/js/cidr.js

@ -8,11 +8,11 @@
var net_inp = this.find('#id_network');
var mask_inp = this.find('#id_mask');
/*var wrsi = this.children('#id_work_range_start_ip');
var wrei = this.children('#id_work_range_end_ip');*/
var validate_ip_by_key = function(){
var v = this.value;
if(v === undefined)
return;
var o = $(this).closest('.form-group-sm,.form-group');
o.removeClass('has-error has-success');
if(v.match(IP4_REG) !== null){
@ -25,8 +25,7 @@
}else
o.addClass('has-error');
};
var validate_ip_by_focus_lost = function(){
console.log('Lost');
var validate_ip_by_focus = function(){
var v = this.value;
if(v.includes('/')){
var chunks = v.split('/');
@ -38,8 +37,15 @@
}else {
settings.res_label.text(v + '/' + mask_inp.val());
}
$(this).trigger('keyup');
};
net_inp.on('keyup', validate_ip_by_key).on('focusout', validate_ip_by_focus_lost);
net_inp.on('keyup focusin', validate_ip_by_key);
net_inp.on('focusout', validate_ip_by_focus);
var validate_mask = function(){
};
mask_inp.on('change', validate_mask);
};
})(jQuery);

Loading…
Cancel
Save