diff --git a/abonapp/forms.py b/abonapp/forms.py index ef22e0e..c4402ec 100644 --- a/abonapp/forms.py +++ b/abonapp/forms.py @@ -3,6 +3,8 @@ from django import forms from django.contrib.auth.hashers import make_password from random import choice from string import digits, ascii_lowercase + +from nas_app.models import NASModel from . import models from django.conf import settings @@ -41,6 +43,8 @@ class AbonForm(forms.ModelForm): abon_group_queryset = None if abon_group_queryset is not None: self.fields['street'].queryset = abon_group_queryset + if not instance: + self.initial['nas'] = NASModel.objects.filter(default=True).first() username = forms.CharField(max_length=127, required=False, initial=_generate_random_username, widget=forms.TextInput(attrs={ @@ -55,7 +59,7 @@ class AbonForm(forms.ModelForm): class Meta: model = models.Abon - fields = ('username', 'telephone', 'fio', 'group', 'description', 'street', 'house', 'is_active') + fields = ('username', 'telephone', 'fio', 'group', 'description', 'street', 'house', 'is_active', 'nas') widgets = { 'fio': forms.TextInput(attrs={ 'placeholder': _('fio'), diff --git a/abonapp/migrations/0003_abon_nas.py b/abonapp/migrations/0003_abon_nas.py new file mode 100644 index 0000000..5d11f1d --- /dev/null +++ b/abonapp/migrations/0003_abon_nas.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-16 18:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('nas_app', '0001_initial'), + ('abonapp', '0002_auto_20180808_1448'), + ] + + operations = [ + migrations.AddField( + model_name='abon', + name='nas', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='nas_app.NASModel', verbose_name='Network access server'), + ), + ] diff --git a/abonapp/models.py b/abonapp/models.py index f98b455..4e9c247 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -13,7 +13,7 @@ from django.utils import timezone 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 nas_app.nas_managers import AbonStruct, TariffStruct, NasFailedResult, NasNetworkError from group_app.models import Group from djing.lib import LogicError from ip_pool.models import IpLeaseModel, NetworkModel @@ -33,7 +33,7 @@ class AbonLog(models.Model): permissions = ( ('can_view_abonlog', _('Can view subscriber logs')), ) - ordering = ('-date',) + ordering = '-date', def __str__(self): return self.comment @@ -81,7 +81,7 @@ class AbonStreet(models.Model): db_table = 'abon_street' verbose_name = _('Street') verbose_name_plural = _('Streets') - ordering = ('name',) + ordering = 'name', class AbonManager(MyUserManager): @@ -94,13 +94,13 @@ class Abon(BaseAccount): group = models.ForeignKey(Group, 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(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) device = models.ForeignKey('devapp.Device', null=True, blank=True, on_delete=models.SET_NULL) dev_port = models.ForeignKey('devapp.Port', null=True, blank=True, on_delete=models.SET_NULL) is_dynamic_ip = models.BooleanField(default=False) + nas = models.ForeignKey('nas_app.NASModel', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_('Network access server'), default=None) MARKER_FLAGS = ( ('icon_donkey', _('Donkey')), @@ -227,14 +227,51 @@ class Abon(BaseAccount): return AbonStruct(self.pk, abon_addresses, agent_trf, self.is_access()) raise LogicError(_('You have not any active leases')) - def sync_with_nas(self, created: bool) -> Optional[Exception]: + def nas_sync_self(self) -> Optional[Exception]: + """ + Synchronize user with gateway(NAS) + :return: + """ + if self.nas is None: + raise LogicError(_('NAS required')) try: agent_abon = self.build_agent_struct() - tm = Transmitter() - if created: - tm.add_user(agent_abon) - else: - tm.update_user(agent_abon) + mngr = self.nas.get_nas_manager() + mngr.update_user(agent_abon) + except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: + print('ERROR:', e) + return e + except LogicError: + pass + + def nas_add_self(self): + """ + Will add this user to network access server + :return: + """ + if self.nas is None: + raise LogicError(_('NAS required')) + try: + agent_abon = self.build_agent_struct() + mngr = self.nas.get_nas_manager() + mngr.add_user(agent_abon) + except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: + print('ERROR:', e) + return e + except LogicError: + pass + + def nas_remove_self(self): + """ + Will remove this user to network access server + :return: + """ + if self.nas is None: + raise LogicError(_('NAS required')) + try: + agent_abon = self.build_agent_struct() + mngr = self.nas.get_nas_manager() + mngr.remove_user(agent_abon) except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: print('ERROR:', e) return e @@ -422,13 +459,11 @@ class PeriodicPayForId(models.Model): @receiver(post_delete, sender=Abon) def abon_del_signal(sender, **kwargs): - abon = kwargs["instance"] + abon = kwargs.get("instance") + if abon is None: + raise ValueError('Instance does not passed to a signal') try: - ab = abon.build_agent_struct() - if ab is None: - return True - tm = Transmitter() - tm.remove_user(ab) + abon.nas_remove_self() except (NasFailedResult, NasNetworkError, LogicError): return True @@ -445,16 +480,13 @@ def abon_tariff_post_init(sender, **kwargs): @receiver(pre_delete, sender=AbonTariff) def abontariff_pre_delete(sender, **kwargs): - abon_tariff = kwargs["instance"] + abon_tariff = kwargs.get("instance") + if abon_tariff is None: + raise ValueError('Instance does not passed to a signal') try: abon = Abon.objects.get(current_tariff=abon_tariff) - ab = abon.build_agent_struct() - if ab is None: - return True - tm = Transmitter() - tm.remove_user(ab) - except Abon.DoesNotExist: - print('ERROR: Abon.DoesNotExist') - except (NasFailedResult, NasNetworkError, ConnectionResetError) as e: - print('NetErr:', e) + abon.nas_remove_self() + except (NasFailedResult, NasNetworkError, LogicError): return True + except Abon.DoesNotExist: + print('Error: abontariff_pre_delete - user not found') diff --git a/abonapp/templates/abonapp/addAbon.html b/abonapp/templates/abonapp/addAbon.html index b392e90..51beedb 100644 --- a/abonapp/templates/abonapp/addAbon.html +++ b/abonapp/templates/abonapp/addAbon.html @@ -54,6 +54,10 @@ }); + {# Nas server #} + {% bootstrap_icon 'globe' as ic %} + {% bootstrap_field form.nas addon_before=ic %} +
+ +
+ + + + diff --git a/abonapp/urls.py b/abonapp/urls.py index 6ed8d4f..7e419c1 100644 --- a/abonapp/urls.py +++ b/abonapp/urls.py @@ -31,7 +31,8 @@ subscriber_patterns = [ url(r'^periodic_pay$', views.add_edit_periodic_pay, name='add_periodic_pay'), url(r'^periodic_pay(?P\d+)/$', views.add_edit_periodic_pay, name='add_periodic_pay'), url(r'^periodic_pay(?P\d+)/del/$', views.del_periodic_pay, name='del_periodic_pay'), - url(r'^lease/add/$', views.lease_add, name='lease_add') + url(r'^lease/add/$', views.lease_add, name='lease_add'), + url(r'^ping$', views.abon_ping, name='ping') ] group_patterns = [ @@ -44,6 +45,7 @@ group_patterns = [ url(r'^street/edit', views.street_edit, name='street_edit'), url(r'^street/(?P\d+)/delete$', views.street_del, name='street_del'), url(r'^active_networks/$', views.active_nets, name='active_nets'), + url(r'^attach_nas/$', views.attach_nas, name='attach_nas'), url(r'^(?P\w{1,127})/', include(subscriber_patterns)) ] @@ -54,7 +56,6 @@ urlpatterns = [ url(r'^log$', views.LogListView.as_view(), name='log'), url(r'^pay$', views.terminal_pay, name='terminal_pay'), url(r'^debtors$', views.DebtorsListView.as_view(), name='debtors'), - url(r'^ping$', views.abon_ping, name='ping'), url(r'^contacts/vcards/$', views.vcards, name='vcards'), # Api's diff --git a/abonapp/views.py b/abonapp/views.py index 5d86acb..9f6a408 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -18,8 +18,9 @@ from djing.lib import DuplicateEntry from jsonview.decorators import json_view from agent.commands.dhcp import dhcp_commit, dhcp_expiry, dhcp_release +from nas_app.models import NASModel from tariff_app.models import Tariff -from agent import NasFailedResult, Transmitter, NasNetworkError +from nas_app.nas_managers import NasFailedResult, NasNetworkError from . import forms from . import models from devapp.models import Device, Port as DevPort @@ -126,7 +127,6 @@ class AbonCreateView(CreateView): assign_perm("abonapp.can_buy_tariff", me, abon) assign_perm("abonapp.can_view_passport", me, abon) assign_perm('abonapp.can_add_ballance', me, abon) - # abon.sync_with_nas(created=True) messages.success(self.request, _('create abon success msg')) self.abon = abon return super(AbonCreateView, self).form_valid(form) @@ -304,7 +304,7 @@ class AbonHomeUpdateView(UpdateView): def form_valid(self, form): r = super(AbonHomeUpdateView, self).form_valid(form) abon = self.object - res = abon.sync_with_nas(created=False) + res = abon.nas_sync_self() if isinstance(res, Exception): messages.warning(self.request, res) messages.success(self.request, _('edit abon success msg')) @@ -412,7 +412,7 @@ def pick_tariff(request, gid, uname): else: deadline = datetime.strptime(deadline, '%Y-%m-%d %H:%M:%S') abon.pick_tariff(trf, request.user, deadline=deadline, comment=log_comment) - r = abon.sync_with_nas(created=False) + r = abon.nas_sync_self() if r is None: messages.success(request, _('Tariff has been picked')) else: @@ -652,15 +652,22 @@ def charts(request, gid, uname): @login_required @permission_required('abonapp.can_ping') @json_view -def abon_ping(request): +def abon_ping(request, gid, uname): ip = request.GET.get('cmd_param') status = False text = ' %s' % _('no ping') + abon = get_object_or_404(models.Abon, username=uname) try: if ip is None: raise lib.LogicError(_('Ip not passed')) - tm = Transmitter() - ping_result = tm.ping(ip) + + if abon.nas is None: + return { + 'status': 1, + 'dat': ' %s' % _('NAS required') + } + mngr = abon.nas.get_nas_manager() + ping_result = mngr.ping(ip) if ping_result is None: if ping(ip, 10): status = True @@ -1089,8 +1096,11 @@ class EditSibscriberMarkers(UpdateView): @lib.decorators.only_admins def user_session_toggle(request, gid, uname, lease_id, action=None): 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 = Transmitter() + tm = abon.nas.get_nas_manager() try: if action == 'free': try: @@ -1105,6 +1115,8 @@ def user_session_toggle(request, gid, uname, lease_id, action=None): 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) @@ -1127,12 +1139,15 @@ def lease_add(request, gid, uname): 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) - tm = Transmitter() - tm.lease_start( - user=abon.build_agent_struct(), - lease=lease.ip - ) - messages.success(request, _('Ip lease has been created')) + 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) @@ -1153,6 +1168,28 @@ def lease_add(request, gid, uname): }) +@login_required +@lib.decorators.only_admins +def attach_nas(request, gid): + if request.method == 'POST': + gateway_id = lib.safe_int(request.POST.get('gateway')) + if gateway_id: + nas = get_object_or_404(NASModel, pk=gateway_id) + abons = models.Abon.objects.filter(group__id=gid) + if abons.exists(): + abons.update(nas=nas) + messages.success(request, _('Network access server for users in this group, has been updated')) + return redirect('abonapp:group_list') + else: + messages.warning(request, _('Users not found')) + else: + messages.error(request, _('You must select gateway')) + return render(request, 'abonapp/modal_attach_nas.html', { + 'gid': gid, + 'nas_list': NASModel.objects.all().iterator() + }) + + # API's @login_required @lib.decorators.only_admins diff --git a/agent/__init__.py b/agent/__init__.py index 5f1b1bd..e69de29 100644 --- a/agent/__init__.py +++ b/agent/__init__.py @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from .mod_mikrotik import MikrotikTransmitter -from .core import NasFailedResult, NasNetworkError -from .structs import TariffStruct, AbonStruct - -# Transmitter мы будем импортировать в других местах -# Тут надо указать какой у нас будет NAS -# т.е. какой класс будет управлять доступом в интернет -# TODO: Transmitter can be lazy init -Transmitter = MikrotikTransmitter diff --git a/agent/commands/dhcp.py b/agent/commands/dhcp.py index 1bef7e3..aac21f8 100644 --- a/agent/commands/dhcp.py +++ b/agent/commands/dhcp.py @@ -24,7 +24,7 @@ def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: i 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.sync_with_nas(created=False) + abon.nas_sync_self() else: return 'User %s is not access to service' % abon.username else: @@ -50,7 +50,7 @@ def dhcp_expiry(client_ip: str) -> Optional[str]: 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.nas_sync_self() except IpLeaseModel.DoesNotExist: pass diff --git a/agent/netflow/mysql_install.sql b/agent/netflow/mysql_install.sql deleted file mode 100644 index 0d1f0f3..0000000 --- a/agent/netflow/mysql_install.sql +++ /dev/null @@ -1,11 +0,0 @@ -DROP TABLE `flowcache`; - -CREATE TABLE `flowcache` ( - `last_time` INT(10) UNSIGNED NOT NULL, - `abon_id` INT(11) DEFAULT NULL UNIQUE, - `octets` INT(10) UNSIGNED NOT NULL, - `packets` INT(10) UNSIGNED NOT NULL, - KEY `flowcache_abon_id_91e1085d` (`abon_id`) -) - ENGINE = MEMORY - DEFAULT CHARSET = utf8; diff --git a/agent/settings.py.example b/agent/settings.py.example deleted file mode 100644 index 97b06a4..0000000 --- a/agent/settings.py.example +++ /dev/null @@ -1,13 +0,0 @@ -# Setting NAS module in __init__.py - -# Certificates -#CERTFILE = "/etc/ssl/server.crt" -#KEYFILE = "/etc/ssl/server.key" - -#IS_USE_SSL = False - -# information for access on NAS server -NAS_IP = '' -NAS_LOGIN = 'admin' -NAS_PASSW = '' -NAS_PORT = 8728 diff --git a/clientsideapp/views.py b/clientsideapp/views.py index 68f9cfe..a8cac8a 100644 --- a/clientsideapp/views.py +++ b/clientsideapp/views.py @@ -9,7 +9,7 @@ from abonapp.models import AbonLog, InvoiceForPayment, Abon from tariff_app.models import Tariff from taskapp.models import Task from djing.lib import LogicError -from agent import NasFailedResult, NasNetworkError +from nas_app.nas_managers import NasFailedResult, NasNetworkError @login_required @@ -53,7 +53,7 @@ def buy_service(request, srv_id): if request.method == 'POST': abon.pick_tariff(service, None, _("Buy the service via user side, service '%s'") % service) - abon.sync_with_nas(created=False) + abon.nas_sync_self() messages.success(request, _("The service '%s' wan successfully activated") % service.title) else: return render_to_text('clientsideapp/modal_service_buy.html', { diff --git a/djing/local_settings.py.template b/djing/local_settings.py.template index c8beba6..a564159 100644 --- a/djing/local_settings.py.template +++ b/djing/local_settings.py.template @@ -16,7 +16,7 @@ SECRET_KEY = '!!!!!!!!!!!!!!!!!!!!!!!!YOUR SECRET KEY!!!!!!!!!!!!!!!!!!!!!!!!' DATABASES = { 'default': { 'OPTIONS': { - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", + 'init_command': "SET sql_mode='STRICT_TRANS_TABLES', default_storage_engine=INNODB", 'isolation_level': 'read uncommitted' }, #'ENGINE': 'django.db.backends.sqlite3', diff --git a/djing/settings.py b/djing/settings.py index a4febba..b56e4c3 100644 --- a/djing/settings.py +++ b/djing/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'ip_pool', 'accounts_app', + 'nas_app', 'abonapp', 'tariff_app', 'searchapp', diff --git a/djing/urls.py b/djing/urls.py index 9c65f70..5d96007 100644 --- a/djing/urls.py +++ b/djing/urls.py @@ -17,7 +17,8 @@ urlpatterns = [ url(r'^msg/', include('msg_app.urls', namespace='msg_app')), url(r'^dialing/', include('dialing_app.urls', namespace='dialapp')), url(r'^groups/', include('group_app.urls', namespace='group_app')), - url(r'^ip_pool/', include('ip_pool.urls', namespace='ip_pool')) + url(r'^ip_pool/', include('ip_pool.urls', namespace='ip_pool')), + url(r'^nas/', include('nas_app.urls', namespace='nas_app')) # Switch language #url(r'^i18n/', include('django.conf.urls.i18n')), diff --git a/docs/dev.md b/docs/dev.md index 78e0ffa..d338d59 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -150,7 +150,7 @@ def terminal_pay(request): Сейчас биллинг работает с Mikrotik в роли устройства для доступа абонентов в интернет. Как можно реализовать такой-же для вашего роутера, например на GNU/Linux. -Создадим файл *agent/mod_linux.py* и реализуем потомка для интерфейса *BaseTransmitter*. +Создадим файл *nas_app/nas_managers/mod_linux.py* и реализуем потомка для интерфейса *BaseTransmitter*. Методы вашего класса будут вызываться биллингом для взаимодействия с сервером доступа абонентов в интернет(NAS). ```python @@ -229,7 +229,7 @@ class LinuxTransmitter(BaseTransmitter): """ ``` -Для того чтоб биллинг знал о вашем классе надо указать его в *agent/\_\_init\_\_.py*. +Для того чтоб биллинг знал о вашем классе надо указать его в *nas_app/nas_managers/\_\_init\_\_.py*. Замените >from .mod_mikrotik import MikrotikTransmitter @@ -249,27 +249,13 @@ from .structs import TariffStruct, AbonStruct Transmitter = LinuxTransmitter ``` -Для примера, как вы наверное уже догадались, можно посмотреть реализацию для Mikrotik в файле *agent/mod_mikrotik.py* +Для примера, как вы наверное уже догадались, можно посмотреть реализацию для Mikrotik в файле *nas_app/nas_managers/mod_mikrotik.py* Чтобы выводить в биллинге различные сообщения об ошибках есть 2 типа исключений: *NasFailedResult* и *NasNetworkError*. NasNetworkError, как понятно из названия, вызывается при проблемах в сети. А NasFailedResult при ошибочных кодах возврата из модуля на сервере NAS. Биллинг прослушивает эти исключения при выполнении, и при возбуждении этих исключений отображает текст ошибки на экране пользователя. -При переопределении базового класса пожалуйста не забывайте вызвать базовый метод чтоб отработали декораторы методов -интерфейса, этот декоратор проверяет тип входных данных. -Динамическая типизация python иногда подкладывает свинью в том смысле что можно передать не то что вы хотели бы передать, -потому типы лучше проконтролировать, и тогда интерпретатор станет вашим другом помошником :) - -Когда я прошу вызвать базовый метод, я имею ввиду это: -```python -... -def add_user_range(self, user_list): - super(LinuxTransmitter, self).add_user_range(user_list) - # ваш код -... -``` - Кстати, не все методы обязательно реализовывать, некоторые из них зарезервированы на будущие цели, в комментариях к их прототипам в интерфейсе *BaseTransmitter* это сказано. Поэтому просто переопределите эти зарезервированные методы как пустые, например метод *add_tariff_range* нигде в биллинге пока не вызывается. так что можно определить его пустым. diff --git a/group_app/models.py b/group_app/models.py index c915f0f..7328553 100644 --- a/group_app/models.py +++ b/group_app/models.py @@ -8,8 +8,7 @@ class Group(models.Model): code = models.CharField(_('Tech code'), blank=True, max_length=12) def get_absolute_url(self): - url = resolve_url('group_app:edit', self.pk) - return url + return resolve_url('group_app:edit', self.pk) class Meta: db_table = 'groups' diff --git a/nas_app/__init__.py b/nas_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nas_app/admin.py b/nas_app/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nas_app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nas_app/apps.py b/nas_app/apps.py new file mode 100644 index 0000000..3f7eacf --- /dev/null +++ b/nas_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NasAppConfig(AppConfig): + name = 'nas_app' diff --git a/nas_app/forms.py b/nas_app/forms.py new file mode 100644 index 0000000..64285db --- /dev/null +++ b/nas_app/forms.py @@ -0,0 +1,28 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from nas_app.models import NASModel +from djing import IP_ADDR_REGEX + + +class NasForm(forms.ModelForm): + + def clean_default(self): + cd = self.cleaned_data + default = cd.get('default') + if default: + try: + NASModel.objects.get(default=True) + raise ValidationError(message=_('Can be only one default gateway'), code='unique') + except NASModel.DoesNotExist: + pass + return default + + class Meta: + model = NASModel + fields = '__all__' + widgets = { + 'ip_address': forms.TextInput(attrs={ + 'pattern': IP_ADDR_REGEX + }) + } diff --git a/nas_app/migrations/0001_initial.py b/nas_app/migrations/0001_initial.py new file mode 100644 index 0000000..13b62f7 --- /dev/null +++ b/nas_app/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-08-17 17:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='NASModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=127, unique=True, verbose_name='Title')), + ('ip_address', models.GenericIPAddressField(unique=True, verbose_name='Ip address')), + ('ip_port', models.PositiveSmallIntegerField(verbose_name='Port')), + ('auth_login', models.CharField(max_length=64, verbose_name='Login')), + ('auth_passw', models.CharField(max_length=127, verbose_name='Password')), + ('nas_type', models.CharField(choices=[('mktk', 'Mikrotik NAS')], default='mktk', max_length=4, verbose_name='Type')), + ('default', models.BooleanField(default=False, verbose_name='Is default')), + ], + options={ + 'verbose_name': 'Network access server. Gateway', + 'verbose_name_plural': 'Network access servers. Gateways', + 'db_table': 'nas', + 'ordering': ('ip_address',), + 'permissions': (('can_view_nas', 'Can view NAS'),), + }, + ), + ] diff --git a/nas_app/migrations/__init__.py b/nas_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nas_app/models.py b/nas_app/models.py new file mode 100644 index 0000000..9775702 --- /dev/null +++ b/nas_app/models.py @@ -0,0 +1,63 @@ +from django.contrib.messages import MessageFailure +from django.db.models.signals import pre_delete +from django.dispatch import receiver +from django.shortcuts import resolve_url +from django.utils.translation import gettext_lazy as _ +from django.db import models +from djing.lib import MyChoicesAdapter +from nas_app.nas_managers import NAS_TYPES + + +class NASModel(models.Model): + title = models.CharField(_('Title'), max_length=127, unique=True) + ip_address = models.GenericIPAddressField(_('Ip address'), unique=True) + ip_port = models.PositiveSmallIntegerField(_('Port')) + auth_login = models.CharField(_('Login'), max_length=64) + auth_passw = models.CharField(_('Password'), max_length=127) + nas_type = models.CharField(_('Type'), max_length=4, choices=MyChoicesAdapter(NAS_TYPES), default=NAS_TYPES[0][0]) + default = models.BooleanField(_('Is default'), default=False) + + def get_nas_manager_klass(self): + try: + return next(klass for code, klass in NAS_TYPES if code == self.nas_type) + except StopIteration: + raise TypeError(_('One of nas types implementation is not found')) + + def get_nas_manager(self): + klass = self.get_nas_manager_klass() + if hasattr(self, '_nas_mngr'): + o = getattr(self, '_nas_mngr') + setattr(self, '_nas_mngr', o) + else: + o = klass( + login=self.auth_login, + password=self.auth_passw, + ip=self.ip_address, + port=int(self.ip_port) + ) + setattr(self, '_nas_mngr', o) + return o + + def get_absolute_url(self): + return resolve_url('nas_app:edit', self.pk) + + def __str__(self): + return self.title + + class Meta: + db_table = 'nas' + verbose_name = _('Network access server. Gateway') + verbose_name_plural = _('Network access servers. Gateways') + ordering = 'ip_address', + permissions = ( + ('can_view_nas', _('Can view NAS')), + ) + + +@receiver(pre_delete, sender=NASModel) +def nas_pre_delete(sender, **kwargs): + nas = kwargs.get("instance") + # check if this nas is default. + # You cannot remove default server + if nas.default: + raise MessageFailure(_('You cannot remove default server')) diff --git a/nas_app/nas_managers/__init__.py b/nas_app/nas_managers/__init__.py new file mode 100644 index 0000000..313bf7e --- /dev/null +++ b/nas_app/nas_managers/__init__.py @@ -0,0 +1,9 @@ +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 + +# Указываем какие реализации NAS у нас есть, это будет использоваться в +# web интерфейсе +NAS_TYPES = ( + ('mktk', MikrotikTransmitter), +) diff --git a/agent/core.py b/nas_app/nas_managers/core.py similarity index 85% rename from agent/core.py rename to nas_app/nas_managers/core.py index 130420c..777f7f3 100644 --- a/agent/core.py +++ b/nas_app/nas_managers/core.py @@ -1,7 +1,7 @@ -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod, abstractproperty from typing import Iterator, Any, Tuple, Optional - -from .structs import AbonStruct, TariffStruct, VectorAbon, VectorTariff +from djing import ping +from nas_app.nas_managers.structs import AbonStruct, TariffStruct, VectorAbon, VectorTariff # Raised if NAS has returned failed result @@ -16,6 +16,22 @@ class NasNetworkError(Exception): # Communicate with NAS class BaseTransmitter(ABC): + @abstractproperty + def description(self): + """ + :return: Returnd a description of nas implementation + """ + + def __init__(self, login: str, password: str, ip: str, port: int, *args, **kwargs): + if not ping(ip): + raise NasNetworkError('NAS %(ip_addr)s does not pinged' % { + 'ip_addr': ip + }) + + @classmethod + def get_description(cls): + return cls.description + @abstractmethod def add_user_range(self, user_list: VectorAbon): """add subscribers list to NAS""" diff --git a/agent/mod_mikrotik.py b/nas_app/nas_managers/mod_mikrotik.py similarity index 94% rename from agent/mod_mikrotik.py rename to nas_app/nas_managers/mod_mikrotik.py index a29173f..2534183 100644 --- a/agent/mod_mikrotik.py +++ b/nas_app/nas_managers/mod_mikrotik.py @@ -1,6 +1,6 @@ +import binascii import re import socket -import binascii from abc import ABCMeta from hashlib import md5 from ipaddress import _BaseAddress, ip_address @@ -8,12 +8,9 @@ from typing import Iterable, Optional, Tuple, Generator, Dict from django.conf import settings from django.utils.translation import ugettext_lazy as _ - from djing.lib.decorators import LazyInitMetaclass -from .structs import TariffStruct, AbonStruct, VectorAbon, VectorTariff -from . import settings as local_settings -from djing import ping -from agent.core import BaseTransmitter, NasNetworkError, NasFailedResult +from nas_app.nas_managers.core import BaseTransmitter, NasNetworkError, NasFailedResult +from nas_app.nas_managers.structs import TariffStruct, AbonStruct, VectorAbon, VectorTariff DEBUG = getattr(settings, 'DEBUG', False) @@ -29,8 +26,6 @@ class ApiRos(object): def __init__(self, ip: str, port: int): if self.sk is None: sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if port is None: - port = local_settings.NAS_PORT sk.connect((ip, port or 8728)) self.sk = sk @@ -168,20 +163,16 @@ class ApiRos(object): class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs', (ABCMeta, LazyInitMetaclass), {})): - def __init__(self, login=None, password=None, ip=None, port=None): - ip = ip or getattr(local_settings, 'NAS_IP') - if ip is None or ip == '': - raise NasNetworkError('Ip address of NAS does not specified') - if not ping(ip): - raise NasNetworkError('NAS %(ip_addr)s does not pinged' % { - 'ip_addr': ip - }) + description = _('Mikrotik NAS') + + def __init__(self, login: str, password: str, ip: str, port: int, *args, **kwargs): try: - super(MikrotikTransmitter, self).__init__(ip, port) - self.login( - login or getattr(local_settings, 'NAS_LOGIN'), - password or getattr(local_settings, 'NAS_PASSW') - ) + ApiRos.__init__(self, ip, port) + MikrotikTransmitter.__init__(self, + login=login, password=password, ip=ip, + port=port, *args, **kwargs + ) + self.login(username=login, pwd=password) except ConnectionRefusedError: raise NasNetworkError('Connection to %s is Refused' % ip) @@ -280,7 +271,7 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs '=burst-time=1/1' )) - def remove_queue(self, user: AbonStruct, queue: AbonStruct=None) -> None: + def remove_queue(self, user: AbonStruct, queue: AbonStruct = None) -> None: if not isinstance(user, AbonStruct): raise TypeError if queue is None: @@ -405,10 +396,12 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs 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) diff --git a/agent/structs.py b/nas_app/nas_managers/structs.py similarity index 100% rename from agent/structs.py rename to nas_app/nas_managers/structs.py diff --git a/nas_app/templates/nas_app/nasmodel_add.html b/nas_app/templates/nas_app/nasmodel_add.html new file mode 100644 index 0000000..56e0cf8 --- /dev/null +++ b/nas_app/templates/nas_app/nasmodel_add.html @@ -0,0 +1,31 @@ +{% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n %} +{% load bootstrap3 %} + +{% block breadcrumb %} + +{% endblock %} + +{% block page-header %} + {% trans 'Add new gateway' %} +{% endblock %} + +{% block main %} +
+
+

{% trans 'Add gateway' %}

+
+
+
{% csrf_token %} + {% bootstrap_form form %} + +
+
+
+{% endblock %} diff --git a/nas_app/templates/nas_app/nasmodel_confirm_delete.html b/nas_app/templates/nas_app/nasmodel_confirm_delete.html new file mode 100644 index 0000000..d56f8d0 --- /dev/null +++ b/nas_app/templates/nas_app/nasmodel_confirm_delete.html @@ -0,0 +1,17 @@ +{% extends 'base_delete_modal.html' %} +{% load i18n %} + +{% block modal_form_url %} + {% url 'nas_app:del' object.pk %} +{% endblock %} + +{% block modal_form_title %} + {% trans 'Remove NAS' %} +{% endblock %} + +{% block modal_form_text %} + {% blocktrans trimmed %} +

If you remove this server, then all users than has been + attached to them will lost parent NAS server.

+ {% endblocktrans %} +{% endblock %} \ No newline at end of file diff --git a/nas_app/templates/nas_app/nasmodel_list.html b/nas_app/templates/nas_app/nasmodel_list.html new file mode 100644 index 0000000..ae70e07 --- /dev/null +++ b/nas_app/templates/nas_app/nasmodel_list.html @@ -0,0 +1,60 @@ +{% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n %} + +{% block breadcrumb %} + +{% endblock %} + +{% block page-header %} + {% trans 'Gateways' %} +{% endblock %} + +{% block main %} +
+ {% for nas in object_list %} +
+
+
+

dasdasd

+
+
+
+
{% trans 'Title' %}
{{ nas.title }}
+
{% trans 'Ip address' %}
{{ nas.ip_address }}
+
{% trans 'Port' %}
{{ nas.ip_port }}
+
{% trans 'Auth login' %}
{{ nas.auth_login }}
+
{% trans 'Auth password' %}
{{ nas.auth_passw }}
+
{% trans 'NAS type' %}
{{ nas.get_nas_type_display }}
+
{% trans 'Is default' %}
+
+ +
+
+
+ +
+
+ {% empty %} +
+

{% trans "You don't have gateways" %}

+
+ {% endfor %} + +
+{% endblock %} diff --git a/nas_app/templates/nas_app/nasmodel_update.html b/nas_app/templates/nas_app/nasmodel_update.html new file mode 100644 index 0000000..c2ffcb5 --- /dev/null +++ b/nas_app/templates/nas_app/nasmodel_update.html @@ -0,0 +1,42 @@ +{% extends request.is_ajax|yesno:'bajax.html,base.html' %} +{% load i18n %} +{% load bootstrap3 %} + +{% block breadcrumb %} + +{% endblock %} + +{% block page-header %} + {% trans 'Change gateways' %} +{% endblock %} + +{% block main %} +
+
+

{% trans 'Change gateway' %}

+
+
+
{% csrf_token %} + {% bootstrap_form form %} +
+ + {% if perms.nas_app.delete_nasmodel %} + + {% trans 'Delete' %} + + {% else %} + + {% trans 'Delete' %} + + {% endif %} +
+
+
+
+{% endblock %} diff --git a/nas_app/tests.py b/nas_app/tests.py new file mode 100644 index 0000000..1f104ca --- /dev/null +++ b/nas_app/tests.py @@ -0,0 +1,200 @@ +from abc import ABCMeta + +from abonapp.models import Abon +from accounts_app.models import UserProfile +from django.conf import settings +from django.utils.translation import gettext_lazy as _, gettext +from django.shortcuts import resolve_url +from django.test import TestCase +from group_app.models import Group +from nas_app.models import NASModel +from nas_app.nas_managers import MikrotikTransmitter + + +class MyBaseTestCase(metaclass=ABCMeta): + def _client_get_check_login(self, url): + """ + Checks if url is protected from unauthorized access + :param url: + :return: authorized response + """ + r = self.client.get(url) + self.assertRedirects(r, "%s?next=%s" % (getattr(settings, 'LOGIN_URL'), url)) + self.client.force_login(self.adminuser) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + return r + + def setUp(self): + grp = Group.objects.create(title='Grp1') + a1 = Abon.objects.create_user( + telephone='+79781234567', + username='abon', + password='passw1' + ) + a1.group = grp + a1.save(update_fields=('group',)) + my_admin = UserProfile.objects.create_superuser('+79781234567', 'local_superuser', 'ps') + self.adminuser = my_admin + self.abon = a1 + self.group = grp + + +class NASModelTestCase(MyBaseTestCase, TestCase): + def setUp(self): + super(NASModelTestCase, self).setUp() + nas = NASModel.objects.create( + title='Title', + ip_address='192.168.8.12', + ip_port=123, + auth_login='admin', + auth_passw='admin', + default=True, + nas_type='mktk' + ) + self.nas = nas + + def test_create(self): + url = resolve_url('nas_app:add') + self._client_get_check_login(url) + + # test success create nas + r = self.client.post(url, data={ + 'title': 'Test success nas', + 'ip_address': '192.168.8.10', + 'ip_port': 1254, + 'auth_login': '_', + 'auth_passw': '_', + 'nas_type': 'mktk' + }) + self.assertEqual(r.status_code, 302) + msg = r.cookies.get('messages') + self.assertIn(gettext('New NAS has been created'), msg.output()) + NASModel.objects.get(title='Test success nas', ip_address='192.168.8.10', ip_port=1254, + auth_login='_', auth_passw='_') + + # test error ip_port big range + r = self.client.post(url, data={ + 'title': 'New nas', + 'ip_address': '192.168.8.13', + 'ip_port': 8755877855798, + 'auth_login': '_', + 'auth_passw': '_' + }) + self.assertEqual(r.status_code, 200) + self.assertFormError(response=r, form='form', field='ip_port', + errors=_('Ensure this value is less than or equal to %(limit_value)s.') % { + 'limit_value': 65535 + }) + + # test get request + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + + # test error duplicates title + r = self.client.post(url, data={ + 'title': 'Test success nas', + 'ip_address': '192.168.8.14', + 'ip_port': 2543, + 'auth_login': '_w', + 'auth_passw': '_v' + }) + self.assertEqual(r.status_code, 200) + self.assertFormError(response=r, form='form', field='title', + errors=_('%(model_name)s with this %(field_label)s already exists.') % { + 'model_name': NASModel._meta.verbose_name, + 'field_label': NASModel._meta.get_field('title').verbose_name + }) + + # test error duplicates default + r = self.client.post(url, data={ + 'title': 'New again nas', + 'ip_address': '192.168.8.15', + 'ip_port': 9873, + 'auth_login': '_w', + 'auth_passw': '_v', + 'default': True + }) + self.assertEqual(r.status_code, 200) + self.assertFormError(response=r, form='form', field='default', errors=_('Can be only one default gateway')) + + # test error duplicates ip_address + r = self.client.post(url, data={ + 'title': 'New again nas2', + 'ip_address': '192.168.8.10', + 'ip_port': 1254, + 'auth_login': '_w', + 'auth_passw': '_v' + }) + self.assertEqual(r.status_code, 200) + self.assertFormError(response=r, form='form', field='ip_address', + errors=_('%(model_name)s with this %(field_label)s already exists.') % { + 'model_name': NASModel._meta.verbose_name, + 'field_label': NASModel._meta.get_field('ip_address').verbose_name + }) + + def test_change(self): + url = resolve_url('nas_app:edit', 1) + self._client_get_check_login(url) + + # test get request + self.client.get(url) + + # test success change + r = self.client.post(url, data={ + 'title': 'New again nas2 changed', + 'ip_address': '192.168.8.12', + 'ip_port': 7865, + 'auth_login': '_w_c', + 'auth_passw': '_v_c', + 'nas_type': 'mktk' + }) + self.assertRedirects(r, resolve_url('nas_app:edit', 1)) + msg = r.cookies.get('messages') + self.assertIn(gettext('Update successfully'), msg.output()) + NASModel.objects.get(title='New again nas2 changed', ip_address='192.168.8.12', + ip_port=7865, auth_login='_w_c', auth_passw='_v_c') + + def test_delete(self): + url = resolve_url('nas_app:add') + self._client_get_check_login(url) + r = self.client.post(url, data={ + 'title': 'Test success nas_2', + 'ip_address': '192.168.8.11', + 'ip_port': 1254, + 'auth_login': '_', + 'auth_passw': '_', + 'nas_type': 'mktk' + }) + self.assertEqual(r.status_code, 302) + o = NASModel.objects.get(title='Test success nas_2', ip_address='192.168.8.11', ip_port=1254, + auth_login='_', auth_passw='_') + url = resolve_url('nas_app:del', o.pk) + + # test get request + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + + # test deleting + r = self.client.post(url) + self.assertRedirects(r, resolve_url('nas_app:home')) + msg = r.cookies.get('messages') + self.assertIn(gettext('Server successfully removed'), msg.output()) + try: + NASModel.objects.get(title='Test success nas_2') + raise self.failureException("NAS not removed") + except NASModel.DoesNotExist: + pass + + # try to remove default nas + nas_id = self.nas.pk + r = self.client.post(resolve_url('nas_app:del', nas_id)) + self.assertRedirects(r, expected_url=resolve_url('nas_app:edit', nas_id)) + msg = r.cookies.get('messages') + self.assertIn(gettext('You cannot remove default server'), msg.output()) + + def test_get_nas_manager(self): + r = self.nas.get_nas_manager_klass() + self.assertIs(r, MikrotikTransmitter) + r = self.nas.get_nas_manager() + self.assertIsInstance(r, MikrotikTransmitter) diff --git a/nas_app/urls.py b/nas_app/urls.py new file mode 100644 index 0000000..040970f --- /dev/null +++ b/nas_app/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url +from nas_app import views + + +app_name = 'nas_app' + + +urlpatterns = [ + url(r'^$', view=views.NasListView.as_view(), name='home'), + url(r'^add$', view=views.NasCreateView.as_view(), name='add'), + url(r'^(?P\d+)/del$', views.NasDeleteView.as_view(), name='del'), + url(r'^(?P\d+)/edit$', views.NasUpdateView.as_view(), name='edit'), +] diff --git a/nas_app/views.py b/nas_app/views.py new file mode 100644 index 0000000..4a4bc6c --- /dev/null +++ b/nas_app/views.py @@ -0,0 +1,66 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.messages import MessageFailure +from django.http import HttpResponseRedirect +from django.shortcuts import resolve_url +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse_lazy +from django.views.generic import ListView, CreateView, DeleteView, UpdateView +from guardian.decorators import permission_required_or_403 as permission_required +from guardian.shortcuts import assign_perm +from nas_app.forms import NasForm +from nas_app.models import NASModel + + +@method_decorator(login_required, name='dispatch') +class NasListView(ListView): + model = NASModel + + +@method_decorator(login_required, name='dispatch') +@method_decorator(permission_required('nas_app.add_nasmodel'), name='dispatch') +class NasCreateView(CreateView): + model = NASModel + form_class = NasForm + template_name = 'nas_app/nasmodel_add.html' + success_url = reverse_lazy('nas_app:home') + + 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.can_view_nas", self.request.user, self.object) + messages.success(self.request, _('New NAS has been created')) + return r + + +@method_decorator(login_required, name='dispatch') +@method_decorator(permission_required('nas_app.delete_nasmodel'), name='dispatch') +class NasDeleteView(DeleteView): + model = NASModel + success_url = reverse_lazy('nas_app:home') + pk_url_kwarg = 'nas_id' + + def delete(self, request, *args, **kwargs): + try: + r = super(NasDeleteView, self).delete(request, *args, **kwargs) + messages.success(request, _('Server successfully removed')) + return r + except MessageFailure as e: + messages.error(request, e) + failure_url = resolve_url('nas_app:edit', self.object.pk) + return HttpResponseRedirect(failure_url) + + +@method_decorator(login_required, name='dispatch') +@method_decorator(permission_required('nas_app.change_nasmodel'), name='dispatch') +class NasUpdateView(UpdateView): + model = NASModel + form_class = NasForm + pk_url_kwarg = 'nas_id' + template_name = 'nas_app/nasmodel_update.html' + + def form_valid(self, form): + r = super(NasUpdateView, self).form_valid(form) + messages.success(self.request, _('Update successfully')) + return r diff --git a/periodic.py b/periodic.py index 45e6776..2394c15 100755 --- a/periodic.py +++ b/periodic.py @@ -9,7 +9,7 @@ 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 agent import Transmitter, NasNetworkError, NasFailedResult +from nas_app.nas_managers import NasNetworkError, NasFailedResult from djing.lib import LogicError diff --git a/statistics/migrations/0003_auto_20180814_1921.py b/statistics/migrations/0003_auto_20180814_1921.py index 4486464..e932341 100644 --- a/statistics/migrations/0003_auto_20180814_1921.py +++ b/statistics/migrations/0003_auto_20180814_1921.py @@ -2,11 +2,11 @@ # Generated by Django 1.11 on 2018-08-14 19:21 from __future__ import unicode_literals -from django.db import migrations +from django.db import migrations, models +from statistics.fields import UnixDateTimeField class Migration(migrations.Migration): - dependencies = [ ('abonapp', '0002_auto_20180808_1448'), ('statistics', '0002_auto_20180808_1236'), @@ -14,13 +14,30 @@ class Migration(migrations.Migration): operations = [ migrations.RunSQL( - "DROP TABLE `flowcache`; " - "CREATE TABLE `flowcache` ( " - " `last_time` INT(10) UNSIGNED NOT NULL, " - " `abon_id` INT(11) DEFAULT NULL UNIQUE, " - " `octets` INT(10) UNSIGNED NOT NULL, " - " `packets` INT(10) UNSIGNED NOT NULL, " - " KEY `flowcache_abon_id_91e1085d` (`abon_id`) " - ") ENGINE = MEMORY DEFAULT CHARSET = utf8;" + ( + "DROP TABLE `flowcache`;", + "CREATE TABLE `flowcache` ( " + " `last_time` INT(10) UNSIGNED NOT NULL, " + " `abon_id` INT(11) DEFAULT NULL UNIQUE, " + " `octets` INT(10) UNSIGNED NOT NULL, " + " `packets` INT(10) UNSIGNED NOT NULL, " + " KEY `flowcache_abon_id_91e1085d` (`abon_id`) " + ") ENGINE = MEMORY DEFAULT CHARSET = utf8;" + ), + state_operations=[ + migrations.DeleteModel(name='statcache'), + migrations.CreateModel( + name='statcache', + fields=[ + ('last_time', UnixDateTimeField()), + ('abon', models.OneToOneField('abonapp.Abon', on_delete=models.CASCADE, primary_key=True)), + ('octets', models.PositiveIntegerField(default=0)), + ('packets', models.PositiveIntegerField(default=0)) + ], + options={ + 'db_table': 'flowcache', + }, + ) + ] ) ] diff --git a/templates/base.html b/templates/base.html index 23a8ad6..d04e14c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -100,6 +100,13 @@ + {% url 'nas_app:home' as nashome %} + + + {% trans 'NAS' %} + + +