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

44
abonapp/models.py

@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime
from ipaddress import ip_address
from typing import Optional from typing import Optional
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models, connection, transaction from django.db import models, connection, transaction
from django.db.models.signals import post_delete, pre_delete, post_init 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 accounts_app.models import UserProfile, MyUserManager, BaseAccount
from agent import Transmitter, AbonStruct, TariffStruct, NasFailedResult, NasNetworkError from agent import Transmitter, AbonStruct, TariffStruct, NasFailedResult, NasNetworkError
from group_app.models import Group 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 tariff_app.models import Tariff, PeriodicPay
from bitfield import BitField 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) 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')) group = models.ForeignKey(Group, models.SET_NULL, blank=True, null=True, verbose_name=_('User group'))
ballance = models.FloatField(default=0.0) 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) description = models.TextField(_('Comment'), null=True, blank=True)
street = models.ForeignKey(AbonStreet, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Street')) 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) house = models.CharField(_('House'), max_length=12, null=True, blank=True)
@ -213,23 +215,24 @@ class Abon(BaseAccount):
# make subscriber from agent structure # make subscriber from agent structure
def build_agent_struct(self): 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 return
abon_tariff = self.active_tariff() abon_tariff = self.active_tariff()
if abon_tariff is None: 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]: def sync_with_nas(self, created: bool) -> Optional[Exception]:
agent_abon = self.build_agent_struct() agent_abon = self.build_agent_struct()
@ -245,6 +248,13 @@ class Abon(BaseAccount):
print('ERROR:', e) print('ERROR:', e)
return 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): def get_absolute_url(self):
return resolve_url('abonapp:abon_home', self.group.id, self.username) 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 %} {% bootstrap_field form.telephone form_group_class='form-group-sm' addon_after_class='input-group-btn' addon_after=bt %}
{% endwith %} {% endwith %}
{% bootstrap_field form.is_active form_group_class='form-group-sm' %}
{# Ip address field #} {# 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.street form_group_class='form-group-sm' %}
{% bootstrap_field form.house 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' %} {% bootstrap_field form.group form_group_class='form-group-sm' %}
@ -203,9 +203,13 @@
{% for lease in abon.ip_addresses.all %} {% for lease in abon.ip_addresses.all %}
<li class="list-group-item"> <li class="list-group-item">
{% if lease.is_active %} {% 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 %} {% 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 %} {% endif %}
<span{{ lease.is_active|yesno:', class="text-muted"'|safe }}> <span{{ lease.is_active|yesno:', class="text-muted"'|safe }}>
<b>{{ lease }}</b> <b>{{ lease }}</b>
@ -217,7 +221,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
<div class="panel-footer"> <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="glyphicon glyphicon-plus"></span>
<span class="hidden-xs">{% trans 'Add' %}</span> <span class="hidden-xs">{% trans 'Add' %}</span>
</a> </a>

11
abonapp/templates/abonapp/peoples.html

@ -31,13 +31,7 @@
{% if order_by == 'username' %}<span class="glyphicon glyphicon-filter"></span>{% endif %} {% if order_by == 'username' %}<span class="glyphicon glyphicon-filter"></span>{% endif %}
</th> </th>
<th class="hidden-xs">{% trans 'Last traffic' %}</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' %}"> <a href="{% url 'abonapp:people_list' group.pk %}?{% url_replace request order_by='fio' dir=dir|default:'down' %}">
{% trans 'fio' %} {% trans 'fio' %}
</a> </a>
@ -93,7 +87,6 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </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.fio }}</td>
<td class="col-xs-2">{{ human.street|default:_('Not assigned') }}</td> <td class="col-xs-2">{{ human.street|default:_('Not assigned') }}</td>
<td class="col-xs-1">{{ human.house|default:'-' }}</td> <td class="col-xs-1">{{ human.house|default:'-' }}</td>
@ -123,7 +116,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="12">
<td colspan="11">
{% trans 'Subscribers not found' %}. {% trans 'Subscribers not found' %}.
{% if perms.abonapp.add_abon %} {% if perms.abonapp.add_abon %}
<a href="{% url 'abonapp:add_abon' group.pk %}">{% trans 'Add abon' %}</a> <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/add/$', views.tel_add, name='telephone_new'),
url(r'^tel/del/$', views.tel_del, name='telephone_del'), url(r'^tel/del/$', views.tel_del, name='telephone_del'),
url(r'^markers/$', views.EditSibscriberMarkers.as_view(), name='markers_edit'), 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$', 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+)/$', 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') 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' template_name = 'abonapp/peoples.html'
def get_queryset(self): def get_queryset(self):
# TODO: optimize that query
street_id = lib.safe_int(self.request.GET.get('street')) street_id = lib.safe_int(self.request.GET.get('street'))
gid = lib.safe_int(self.kwargs.get('gid')) gid = lib.safe_int(self.kwargs.get('gid'))
peoples_list = models.Abon.objects.all().select_related('group', 'street', 'current_tariff') 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) peoples_list = peoples_list.filter(group__pk=gid)
try: 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: try:
abon.stat_cache = StatCache.objects.get(ip=abon.ip_address)
abon.stat_cache = StatCache.objects.get(ip__in=ips)
except StatCache.DoesNotExist: except StatCache.DoesNotExist:
pass pass
except lib.LogicError as e: except lib.LogicError as e:
@ -438,9 +440,9 @@ def pick_tariff(request, gid, uname):
@permission_required('abonapp.delete_abontariff') @permission_required('abonapp.delete_abontariff')
def unsubscribe_service(request, gid, uname, abon_tariff_id): def unsubscribe_service(request, gid, uname, abon_tariff_id):
try: 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_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() abon_tariff.delete()
messages.success(request, _('User has been detached from service')) messages.success(request, _('User has been detached from service'))
except NasFailedResult as e: except NasFailedResult as e:
@ -938,18 +940,18 @@ def abon_export(request, gid):
}, request=request) }, 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 @login_required
@ -1040,6 +1042,23 @@ class EditSibscriberMarkers(UpdateView):
return v 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 # API's
@login_required @login_required
@lib.decorators.only_admins @lib.decorators.only_admins
@ -1048,7 +1067,7 @@ def abons(request):
ablist = ({ ablist = ({
'id': abn.pk, 'id': abn.pk,
'tarif_id': abn.active_tariff().tariff.pk if abn.active_tariff() is not None else 0, '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 'is_active': abn.is_active
} for abn in models.Abon.objects.all()) } 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 django.core.exceptions import MultipleObjectsReturned
from abonapp.models import Abon from abonapp.models import Abon
from devapp.models import Device, Port 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]: 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: else:
abon = Abon.objects.get(device=dev) abon = Abon.objects.get(device=dev)
if not abon.is_dynamic_ip: 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(): if abon.is_access():
abon.sync_with_nas(created=False) abon.sync_with_nas(created=False)
else: 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))) 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: 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) 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) return dhcp_expiry(client_ip)

2
agent/core.py

@ -74,7 +74,7 @@ class BaseTransmitter(metaclass=ABCMeta):
""" """
@abstractmethod @abstractmethod
def read_users(self) -> Iterable[AbonStruct]:
def read_users(self) -> VectorAbon:
pass pass
def _diff_users(self, users_from_db: Iterator[Any]) -> Tuple[set, set]: 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 socket
import binascii import binascii
from hashlib import md5 from hashlib import md5
from ipaddress import ip_network
from typing import Iterable, Optional, Tuple, Generator, Dict from typing import Iterable, Optional, Tuple, Generator, Dict
from djing.lib import safe_int
from .structs import TariffStruct, AbonStruct, IpStruct, VectorAbon, VectorTariff from .structs import TariffStruct, AbonStruct, IpStruct, VectorAbon, VectorTariff
from . import settings as local_settings from . import settings as local_settings
from django.conf import settings from django.conf import settings
@ -240,9 +241,9 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
a = AbonStruct( a = AbonStruct(
uid=int(dat['=name'][3:]), uid=int(dat['=name'][3:]),
# FIXME: тут в разных микротиках или =target-addresses или =target # FIXME: тут в разных микротиках или =target-addresses или =target
ip=dat['=target'][:-3],
ips=(int(ip_network(ip).network_address) for ip in dat['=target'].split(',')),
tariff=t, 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'] a.queue_id = dat['=.id']
return a return a
@ -268,24 +269,24 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
'/queue/simple/add', '/queue/simple/add',
'=name=uid%d' % user.uid, '=name=uid%d' % user.uid,
# FIXME: тут в разных микротиках или =target-addresses или =target # 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), '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn),
'=queue=MikroBILL_SFQ/MikroBILL_SFQ', '=queue=MikroBILL_SFQ/MikroBILL_SFQ',
'=burst-time=1/1' '=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): if not isinstance(user, AbonStruct):
raise TypeError 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', '/queue/simple/remove',
'=.id=%d' % queue_id
'=.id=%s' % queue_id
)) ))
print(r)
def remove_queue_range(self, q_ids: Iterable[str]): def remove_queue_range(self, q_ids: Iterable[str]):
self._exec_cmd(('/queue/simple/remove', '=numbers=' + ','.join(q_ids))) self._exec_cmd(('/queue/simple/remove', '=numbers=' + ','.join(q_ids)))
@ -299,18 +300,18 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
if queue is None: if queue is None:
return self.add_queue(user) return self.add_queue(user)
else: else:
mk_id = safe_int(getattr(queue, 'queue_id', 0))
mk_id = getattr(queue, 'queue_id')
cmd = [ cmd = [
'/queue/simple/set', '/queue/simple/set',
'=name=uid%d' % user.uid, '=name=uid%d' % user.uid,
'=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn),
# FIXME: тут в разных микротиках или =target-addresses или =target # FIXME: тут в разных микротиках или =target-addresses или =target
'=target=%s' % user.ip,
'=target=%s' % ','.join(str(i) for i in user.ips),
'=queue=MikroBILL_SFQ/MikroBILL_SFQ', '=queue=MikroBILL_SFQ/MikroBILL_SFQ',
'=burst-time=1/1' '=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) r = self._exec_cmd(cmd)
return r return r
@ -340,13 +341,11 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
'=.id=*' + str(mk_id).replace('*', '') '=.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): def find_ip(self, ip: IpStruct, list_name: str):
if not isinstance(ip, IpStruct): if not isinstance(ip, IpStruct):
@ -380,58 +379,56 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
raise ValueError('*users* is used twice, generator does not fit') 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 = (usr.queue_id for usr in users if usr is not None)
self.remove_queue_range(queue_ids) 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): def add_user(self, user: AbonStruct, *args):
if not isinstance(user.ip, IpStruct):
raise TypeError
if user.tariff is None: if user.tariff is None:
return return
if not isinstance(user.tariff, TariffStruct): if not isinstance(user.tariff, TariffStruct):
raise TypeError 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): def remove_user(self, user: AbonStruct):
self.remove_queue(user) 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): 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) 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 # если нет услуги то её не должно быть и в nas
if user.tariff is None: if user.tariff is None:
if queue is not None: if queue is not None:
self.remove_queue(user)
self.remove_queue(user, queue)
return
if not user.is_access:
return return
# если не найден
if find_res is None:
# добавим запись об абоненте
self.add_ip(LIST_USERS_ALLOWED, user.ip)
# Проверяем шейпер # Проверяем шейпер
if queue is None: if queue is None:
@ -470,15 +467,15 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos):
def remove_tariff(self, tid: int): def remove_tariff(self, tid: int):
pass pass
def read_users(self) -> Iterable[AbonStruct]:
def read_users(self) -> VectorAbon:
# shapes is ShapeItem # 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 # 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 return queues

17
agent/structs.py

@ -64,27 +64,30 @@ class TariffStruct(BaseStruct):
# Абонент из базы # Абонент из базы
class AbonStruct(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.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.tariff = tariff
self.is_active = is_active
self.is_access = is_access
self.queue_id = 0 self.queue_id = 0
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, AbonStruct): if not isinstance(other, AbonStruct):
raise TypeError 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 r = r and self.tariff == other.tariff
return r return r
def __str__(self): 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): 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 # Правило шейпинга в фаере, или ещё можно сказать услуга абонента на NAS

5
djing/settings.py

@ -212,3 +212,8 @@ BOOTSTRAP3 = {
# Field class to use in horizontal forms # Field class to use in horizontal forms
'horizontal_field_class': 'col-md-9', '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 from ip_pool import models
admin.site.register(models.NetworkModel) 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 import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -9,35 +10,14 @@ class NetworkForm(forms.ModelForm):
def clean_network(self): def clean_network(self):
netw = self.data.get('network') netw = self.data.get('network')
mask = self.data.get('mask')
if netw is None: if netw is None:
return return
try: 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') raise ValidationError(e, code='invalid')
class Meta: class Meta:
model = models.NetworkModel model = models.NetworkModel
fields = '__all__' 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 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.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djing.fields import MACAddressField
from ip_pool.fields import GenericIpAddressWithPrefix
class NetworkModel(models.Model): class NetworkModel(models.Model):
_netw_cache = None _netw_cache = None
network = models.GenericIPAddressField(
network = GenericIpAddressWithPrefix(
verbose_name=_('IP network'), verbose_name=_('IP network'),
help_text=_('Ip address of network. For example: 192.168.1.0 or fde8:6789:1234:1::'), help_text=_('Ip address of network. For example: 192.168.1.0 or fde8:6789:1234:1::'),
unique=True 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 = ( NETWORK_KINDS = (
('inet', _('Internet')), ('inet', _('Internet')),
('guest', _('Guest')), ('guest', _('Guest')),
@ -37,17 +28,14 @@ class NetworkModel(models.Model):
('admin', _('Admin')) ('admin', _('Admin'))
) )
kind = models.CharField(_('Kind of network'), max_length=6, choices=NETWORK_KINDS, default='guest') kind = models.CharField(_('Kind of network'), max_length=6, choices=NETWORK_KINDS, default='guest')
description = models.CharField(_('Description'), max_length=64) description = models.CharField(_('Description'), max_length=64)
def __str__(self): 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: 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 return self._netw_cache
def get_absolute_url(self): def get_absolute_url(self):
@ -60,28 +48,61 @@ class NetworkModel(models.Model):
ordering = ('description',) 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) 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 employed_ip_queryset, netw
) if ip != net) ) if ip != net)
return free_ip 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) ip = models.GenericIPAddressField(verbose_name=_('Ip address'), unique=True)
network = models.ForeignKey(NetworkModel, on_delete=models.CASCADE, verbose_name=_('Parent network')) 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): def __str__(self):
return self.ip 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): def clean(self):
ip = IPAddress(self.ip)
ip = ip_address(self.ip)
network = self.network.get_network() network = self.network.get_network()
if ip not in network: if ip not in network:
@ -90,14 +111,15 @@ class EmployedIpModel(models.Model):
'net': network 'net': network
}, code='invalid') }, 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: class Meta:
db_table = 'ip_pool_employed_ip' db_table = 'ip_pool_employed_ip'
verbose_name = _('Employed ip') verbose_name = _('Employed ip')
verbose_name_plural = _('Employed ip addresses') verbose_name_plural = _('Employed ip addresses')
ordering = ('-id',) ordering = ('-id',)
unique_together = ('ip', 'network') 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' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %}
{% block breadcrumb %} {% block breadcrumb %}
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><span class="glyphicon glyphicon-home"></span></li> <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:networks' %}">{% trans 'Ip pool' %}</a></li>
<li><a href="{% url 'ip_pool:net_edit' net.id %}">{{ net }}</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> </ol>
{% endblock %} {% endblock %}
{% block page-header %}{% trans 'Ip list' %}{% endblock %}
{% block page-header %}{% trans 'Ip leases list' %}{% endblock %}
{% block main %} {% block main %}
<div class="table-responsive"> <div class="table-responsive">
@ -19,18 +18,22 @@
<thead> <thead>
<tr> <tr>
<th class="col-sm-5">{% trans 'Ip' %}</th> <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-3">{% trans 'Network' %}</th>
<th class="col-sm-1">{% trans 'Is dynamic' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for ip in object_list %} {% for ip in object_list %}
<tr> <tr>
<td>{{ ip.ip }}</td> <td>{{ ip.ip }}</td>
<td>{{ ip.lease_time|date:'j:n H:i:s' }}</td>
<td>{{ ip.network }}</td> <td>{{ ip.network }}</td>
<td><input type="checkbox" {{ ip.is_dynamic|yesno:'checked,' }}></td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

6
ip_pool/templates/ip_pool/net_add.html

@ -3,9 +3,9 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% load globaltags %} {% 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 %} {% block breadcrumb %}
<ol class="breadcrumb"> <ol class="breadcrumb">

8
ip_pool/templates/ip_pool/net_edit.html

@ -3,9 +3,9 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% load globaltags %} {% 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 %} {% block breadcrumb %}
<ol class="breadcrumb"> <ol class="breadcrumb">
@ -34,7 +34,7 @@
<a href="{% back_url request %}" class="btn btn-default"> <a href="{% back_url request %}" class="btn btn-default">
<span class="glyphicon glyphicon-backward"></span> {% trans 'Back' %} <span class="glyphicon glyphicon-backward"></span> {% trans 'Back' %}
</a> </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="glyphicon glyphicon-eye-open"></span>
<span class="hidden-xs hidden-sm">{% trans 'View employed' %}</span> <span class="hidden-xs hidden-sm">{% trans 'View employed' %}</span>
</a> </a>

4
ip_pool/templates/ip_pool/network_list.html

@ -27,7 +27,7 @@
{% with can_ch_net=perms.ip_pool.change_networkmodel %} {% with can_ch_net=perms.ip_pool.change_networkmodel %}
{% for netw in networks_list %} {% for netw in networks_list %}
<tr> <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_start_ip }}</td>
<td>{{ netw.work_range_end_ip }}</td> <td>{{ netw.work_range_end_ip }}</td>
<td class="btn-group btn-group-sm btn-group-justified"> <td class="btn-group btn-group-sm btn-group-justified">
@ -42,7 +42,7 @@
<span class="hidden-xs hidden-sm">{% trans 'Edit' %}</span> <span class="hidden-xs hidden-sm">{% trans 'Edit' %}</span>
</a> </a>
{% endif %} {% 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="glyphicon glyphicon-eye-open"></span>
<span class="hidden-xs hidden-sm">{% trans 'View employed' %}</span> <span class="hidden-xs hidden-sm">{% trans 'View employed' %}</span>
</a> </a>

2
ip_pool/urls.py

@ -7,7 +7,7 @@ app_name = 'ip_pool'
urlpatterns = [ urlpatterns = [
url('^$', views.NetworksListView.as_view(), name='networks'), url('^$', views.NetworksListView.as_view(), name='networks'),
url('^network_add/$', views.NetworkCreateView.as_view(), name='net_add'), 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'), 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 import transaction
from django.db.models import signals from django.db.models import signals
from abonapp.models import Abon, AbonTariff, abontariff_pre_delete, PeriodicPayForId, AbonLog from abonapp.models import Abon, AbonTariff, abontariff_pre_delete, PeriodicPayForId, AbonLog
from ip_pool.models import IpLeaseModel
from agent import Transmitter, NasNetworkError, NasFailedResult from agent import Transmitter, NasNetworkError, NasFailedResult
from djing.lib import LogicError from djing.lib import LogicError
@ -49,6 +50,10 @@ def main():
for pay in ppays: for pay in ppays:
pay.payment_for_service(now=now) pay.payment_for_service(now=now)
# Remove old inactive ip leases
old_leases = IpLeaseModel.objects.expired()
old_leases.delete()
if __name__ == "__main__": if __name__ == "__main__":
try: try:

16
static/js/cidr.js

@ -8,11 +8,11 @@
var net_inp = this.find('#id_network'); var net_inp = this.find('#id_network');
var mask_inp = this.find('#id_mask'); 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 validate_ip_by_key = function(){
var v = this.value; var v = this.value;
if(v === undefined)
return;
var o = $(this).closest('.form-group-sm,.form-group'); var o = $(this).closest('.form-group-sm,.form-group');
o.removeClass('has-error has-success'); o.removeClass('has-error has-success');
if(v.match(IP4_REG) !== null){ if(v.match(IP4_REG) !== null){
@ -25,8 +25,7 @@
}else }else
o.addClass('has-error'); o.addClass('has-error');
}; };
var validate_ip_by_focus_lost = function(){
console.log('Lost');
var validate_ip_by_focus = function(){
var v = this.value; var v = this.value;
if(v.includes('/')){ if(v.includes('/')){
var chunks = v.split('/'); var chunks = v.split('/');
@ -38,8 +37,15 @@
}else { }else {
settings.res_label.text(v + '/' + mask_inp.val()); 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); })(jQuery);

Loading…
Cancel
Save