diff --git a/abonapp/models.py b/abonapp/models.py index c362370..b3f81b7 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -1,5 +1,4 @@ from datetime import datetime -from ipaddress import ip_address from typing import Optional from django.conf import settings @@ -13,7 +12,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _, gettext from accounts_app.models import UserProfile, MyUserManager, BaseAccount -from nas_app.nas_managers import AbonStruct, TariffStruct, NasFailedResult, NasNetworkError +from nas_app.nas_managers import SubnetQueue, NasFailedResult, NasNetworkError from group_app.models import Group from djing.lib import LogicError from ip_pool.models import NetworkModel @@ -232,14 +231,16 @@ class Abon(BaseAccount): def build_agent_struct(self): if not self.ip_address: return - abon_address = ip_address(self.ip_address) abon_tariff = self.active_tariff() - if abon_tariff is None: - agent_trf = None - else: - trf = abon_tariff.tariff - agent_trf = TariffStruct(trf.id, trf.speedIn, trf.speedOut) - return AbonStruct(self.pk, abon_address, agent_trf, self.is_access()) + if abon_tariff: + abon_tariff = abon_tariff.tariff + return SubnetQueue( + name="uid%d" % self.pk, + network=self.ip_address, + max_limit=(abon_tariff.speedIn, abon_tariff.speedOut), + queue_type=SubnetQueue.QUEUE_LEAF, + is_access=self.is_access() + ) def nas_sync_self(self) -> Optional[Exception]: """ diff --git a/abonapp/views.py b/abonapp/views.py index 73fc7ab..f4aefd7 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -5,7 +5,7 @@ from django.db import IntegrityError, ProgrammingError, transaction, Operational from django.db.models import Count, Q from django.shortcuts import render, redirect, get_object_or_404, resolve_url from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin as PermissionRequiredMixin_django from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect from django.contrib import messages from django.urls import reverse_lazy @@ -90,7 +90,7 @@ class GroupListView(LoginRequiredMixin, OnlyAdminsMixin, OrderedFilteredList): return queryset -class AbonCreateView(AbonappPermissionMixin, CreateView): +class AbonCreateView(LoginRequiredMixin, OnlyAdminsMixin, PermissionRequiredMixin_django, CreateView): permission_required = 'abonapp.add_abon' group = None abon = None @@ -244,7 +244,9 @@ class PayHistoryListView(AbonappPermissionMixin, OrderedFilteredList): template_name = 'abonapp/payHistory.html' def get_permission_object(self): - return self.abon.group + if hasattr(self, 'abon'): + return self.abon.group + return models.Group.objects.filter(pk=self.kwargs.get('gid')).first() def get_queryset(self): abon = get_object_or_404(models.Abon, username=self.kwargs.get('uname')) diff --git a/ip_pool/migrations/0003_add_leases_history_model.py b/ip_pool/migrations/0003_auto_20181015_1430.py similarity index 81% rename from ip_pool/migrations/0003_add_leases_history_model.py rename to ip_pool/migrations/0003_auto_20181015_1430.py index 7df6a78..7870879 100644 --- a/ip_pool/migrations/0003_add_leases_history_model.py +++ b/ip_pool/migrations/0003_auto_20181015_1430.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1 on 2018-10-11 11:55 +# Generated by Django 2.1 on 2018-10-15 14:30 from django.db import migrations, models import djing.fields @@ -34,4 +34,10 @@ class Migration(migrations.Migration): model_name='ipleasemodel', name='is_dynamic', ), + migrations.AddField( + model_name='networkmodel', + name='speed', + field=models.FloatField(default=0.0, verbose_name='Speed for subnet'), + preserve_default=False, + ), ] diff --git a/ip_pool/models.py b/ip_pool/models.py index daf15bd..4ac7c2a 100644 --- a/ip_pool/models.py +++ b/ip_pool/models.py @@ -39,6 +39,8 @@ class NetworkModel(models.Model): ip_start = models.GenericIPAddressField(_('Start work ip range')) ip_end = models.GenericIPAddressField(_('End work ip range')) + speed = models.FloatField(_('Speed for subnet')) + def __str__(self): netw = self.get_network() return "%s: %s" % (self.description, netw.with_prefixlen) diff --git a/nas_app/nas_managers/__init__.py b/nas_app/nas_managers/__init__.py index 313bf7e..72939b6 100644 --- a/nas_app/nas_managers/__init__.py +++ b/nas_app/nas_managers/__init__.py @@ -1,6 +1,6 @@ from nas_app.nas_managers.mod_mikrotik import MikrotikTransmitter from nas_app.nas_managers.core import NasNetworkError, NasFailedResult -from nas_app.nas_managers.structs import TariffStruct, AbonStruct +from nas_app.nas_managers.structs import SubnetQueue # Указываем какие реализации NAS у нас есть, это будет использоваться в # web интерфейсе diff --git a/nas_app/nas_managers/core.py b/nas_app/nas_managers/core.py index b85c5f6..8776d31 100644 --- a/nas_app/nas_managers/core.py +++ b/nas_app/nas_managers/core.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod, abstractproperty from typing import Iterator, Any, Tuple, Optional from djing import ping -from nas_app.nas_managers.structs import AbonStruct, TariffStruct, VectorAbon, VectorTariff +from nas_app.nas_managers.structs import SubnetQueue, VectorQueue # Raised if NAS has returned failed result @@ -33,66 +33,36 @@ class BaseTransmitter(ABC): return cls.description @abstractmethod - def add_user_range(self, user_list: VectorAbon): + def add_user_range(self, queue_list: VectorQueue): """add subscribers list to NAS - :param user_list: Vector of instances of subscribers + :param queue_list: Vector of instances of subscribers """ @abstractmethod - def remove_user_range(self, users: VectorAbon): + def remove_user_range(self, queues): """remove subscribers list - :param users: Vector of instances of subscribers + :param queues: Vector of instances of subscribers """ @abstractmethod - def add_user(self, user: AbonStruct, *args): + def add_user(self, queue: SubnetQueue, *args): """add subscriber - :param user: Subscriber instance + :param queue: Subscriber instance """ @abstractmethod - def remove_user(self, user: AbonStruct): + def remove_user(self, queue: SubnetQueue): """ remove subscriber - :param user: Subscriber instance + :param queue: Subscriber instance """ @abstractmethod - def update_user(self, user: AbonStruct, *args): + def update_user(self, queue: SubnetQueue, *args): """ Update subscriber by uid, you can change everything except its uid. Subscriber will found by UID. - :param user: Subscriber instance - """ - - @abstractmethod - def add_tariff_range(self, tariff_list: VectorTariff): - """Add services list to NAS. - :param tariff_list: Vector of TariffStruct - """ - - @abstractmethod - def remove_tariff_range(self, tariff_list: VectorTariff): - """Remove tariff list by unique id list. - :param tariff_list: Vector of TariffStruct - """ - - @abstractmethod - def add_tariff(self, tariff: TariffStruct): - pass - - @abstractmethod - def update_tariff(self, tariff: TariffStruct): - """ - Update tariff by uid, you can change everything except its uid. - Tariff will found by UID. - :param tariff: Service for update - """ - - @abstractmethod - def remove_tariff(self, tid: int): - """ - :param tid: unique id of tariff. + :param queue: Subscriber instance """ @abstractmethod @@ -105,27 +75,9 @@ class BaseTransmitter(ABC): """ @abstractmethod - def read_users(self) -> VectorAbon: + def read_users(self) -> VectorQueue: pass - # @abstractmethod - # def lease_free(self, user: AbonStruct, lease): - # """ - # Remove ip lease from allowed to network - # :param lease: ip_address for lease - # :param user: Subscriber instance - # :return: - # """ - # - # @abstractmethod - # def lease_start(self, user: AbonStruct, lease): - # """ - # Starts ip lease to allowed to network - # :param lease: ip_address for lease - # :param user: Subscriber instance - # :return: - # """ - def _diff_users(self, users_from_db: Iterator[Any]) -> Tuple[set, set]: """ :param users_from_db: QuerySet of all subscribers that can have service @@ -153,3 +105,9 @@ class BaseTransmitter(ABC): for la in list_for_add: print('\t', la) self.add_user_range(list_for_add) + + +def diff_set(one: set, two: set) -> Tuple[set, set]: + list_for_del = (one ^ two) - one + list_for_add = one - two + return list_for_add, list_for_del diff --git a/nas_app/nas_managers/mod_mikrotik.py b/nas_app/nas_managers/mod_mikrotik.py index d62cefc..84dfa58 100644 --- a/nas_app/nas_managers/mod_mikrotik.py +++ b/nas_app/nas_managers/mod_mikrotik.py @@ -3,14 +3,15 @@ import re import socket from abc import ABCMeta from hashlib import md5 -from ipaddress import _BaseAddress, ip_address -from typing import Iterable, Optional, Tuple, Generator, Dict +from ipaddress import ip_network, _BaseNetwork +from typing import Iterable, Optional, Tuple, Generator, Dict, Iterator, Any from django.conf import settings from django.utils.translation import ugettext_lazy as _ from djing.lib.decorators import LazyInitMetaclass -from nas_app.nas_managers.core import BaseTransmitter, NasNetworkError, NasFailedResult -from nas_app.nas_managers.structs import TariffStruct, AbonStruct, VectorAbon, VectorTariff +from nas_app.nas_managers import core +from nas_app.nas_managers import structs as i_structs +from ip_pool.models import NetworkModel DEBUG = getattr(settings, 'DEBUG', False) @@ -40,7 +41,8 @@ class ApiRos(object): md.update(bytes(pwd, 'utf-8')) md.update(chal) for _ in self.talk_iter(("/login", "=name=" + username, - "=response=00" + binascii.hexlify(md.digest()).decode('utf-8'))): + "=response=00" + binascii.hexlify( + md.digest()).decode('utf-8'))): pass self.is_login = True @@ -100,12 +102,15 @@ class ApiRos(object): self.write_bytes(bytes(((l >> 8) & 0xff, l & 0xff))) elif l < 0x200000: l |= 0xC00000 - self.write_bytes(bytes(((l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff))) + self.write_bytes( + bytes(((l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff))) elif l < 0x10000000: l |= 0xE0000000 - self.write_bytes(bytes(((l >> 24) & 0xff, (l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff))) + self.write_bytes(bytes(((l >> 24) & 0xff, (l >> 16) & 0xff, + (l >> 8) & 0xff, l & 0xff))) else: - self.write_bytes(bytes((0xf0, (l >> 24) & 0xff, (l >> 16) & 0xff, (l >> 8) & 0xff, l & 0xff))) + self.write_bytes(bytes((0xf0, (l >> 24) & 0xff, (l >> 16) & 0xff, + (l >> 8) & 0xff, l & 0xff))) def read_len(self): c = self.read_bytes(1)[0] @@ -144,7 +149,7 @@ class ApiRos(object): while n < len(s): r = self.sk.send(s[n:]) if r == 0: - raise NasFailedResult("connection closed by remote end") + raise core.NasFailedResult("connection closed by remote end") n += r def read_bytes(self, length): @@ -152,7 +157,7 @@ class ApiRos(object): while len(ret) < length: s = self.sk.recv(length - len(ret)) if len(s) == 0: - raise NasFailedResult("connection closed by remote end") + raise core.NasFailedResult("connection closed by remote end") ret += s return ret @@ -162,19 +167,23 @@ class ApiRos(object): self.sk.close() -class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs', (ABCMeta, LazyInitMetaclass), {})): +class MikrotikTransmitter(core.BaseTransmitter, ApiRos, + metaclass=type('_ABC_Lazy_mcs', + (ABCMeta, LazyInitMetaclass), {})): description = _('Mikrotik NAS') - def __init__(self, login: str, password: str, ip: str, port: int, *args, **kwargs): + def __init__(self, login: str, password: str, ip: str, port: int, *args, + **kwargs): try: - BaseTransmitter.__init__(self, - login=login, password=password, ip=ip, - port=port, *args, **kwargs - ) + core.BaseTransmitter.__init__(self, + login=login, password=password, + ip=ip, + port=port, *args, **kwargs + ) ApiRos.__init__(self, ip, port) self.login(username=login, pwd=password) except ConnectionRefusedError: - raise NasNetworkError('Connection to %s is Refused' % ip) + raise core.NasNetworkError('Connection to %s is Refused' % ip) def _exec_cmd(self, cmd: Iterable) -> Dict: if not isinstance(cmd, (list, tuple)): @@ -184,7 +193,7 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs if k == '!done': break elif k == '!trap': - raise NasFailedResult(v.get('=message')) + raise core.NasFailedResult(v.get('=message')) r[k] = v or None return r @@ -195,12 +204,12 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs if k == '!done': break elif k == '!trap': - raise NasFailedResult(v.get('=message')) + raise core.NasFailedResult(v.get('=message')) if v: yield v @staticmethod - def _build_shape_obj(info: Dict) -> AbonStruct: + def _build_shape_obj(info: Dict) -> i_structs.SubnetQueue: # Переводим приставку скорости Mikrotik в Mbit/s def parse_speed(text_speed): text_speed_digit = float(text_speed[:-1] or 0.0) @@ -216,10 +225,8 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs return res speed_out, speed_in = info['=max-limit'].split('/') - t = TariffStruct( - speed_in=parse_speed(speed_in), - speed_out=parse_speed(speed_out) - ) + speed_in = parse_speed(speed_in) + speed_out = parse_speed(speed_out) try: target = info.get('=target') if target is None: @@ -230,19 +237,24 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs disabled = True if disabled == 'true' else False if target and name: # target may be '192.168.0.3/32,192.168.0.2/32' - ip = target.split(',')[0] - if not ip: - return - ip, mask_bits = ip.split('/') - if not ip: + net = target.split(',')[0] + if not net: return - a = AbonStruct( - uid=int(name[3:]), - ip=ip, - tariff=t, - is_access=not disabled + a = i_structs.SubnetQueue( + name=name, + network=net, + max_limit=(speed_in, speed_out), + is_access=not disabled, + queue_id=info.get('=.id') ) - a.queue_id = info.get('=.id') + if name.startswith('uid'): + a.queue_type = i_structs.SubnetQueue.QUEUE_LEAF + elif name.startswith('net_'): + a.queue_type = i_structs.SubnetQueue.QUEUE_SUBNET + elif name == 'queue-root': + a.queue_type = i_structs.SubnetQueue.QUEUE_ROOT + else: + a.queue_type = i_structs.SubnetQueue.QUEUE_UNKNOWN return a except ValueError as e: print('ValueError:', e) @@ -252,64 +264,65 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs ################################################# # Find queue by name - def find_queue(self, name: str) -> Optional[AbonStruct]: + def find_queue(self, name: str) -> Optional[i_structs.SubnetQueue]: r = self._exec_cmd(('/queue/simple/print', '?name=%s' % name)) if r: return self._build_shape_obj(r.get('!re')) - def add_queue(self, user: AbonStruct) -> None: - if not isinstance(user, AbonStruct): - raise TypeError - if user.tariff is None or not isinstance(user.tariff, TariffStruct): - return + def add_queue(self, queue: i_structs.SubnetQueue, + parent_name: str) -> None: + if not isinstance(queue, i_structs.SubnetQueue): + raise TypeError('queue must be instance of SubnetQueue') self._exec_cmd(( '/queue/simple/add', - '=name=uid%d' % user.uid, + '=name=%s' % queue.name, # FIXME: тут в разных микротиках или =target-addresses или =target - '=target=%s' % user.ip, - '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), + '=target=%s' % queue.network, + '=max-limit=%.3fM/%.3fM' % queue.max_limit, '=queue=Djing_SFQ/Djing_SFQ', - '=burst-time=1/1' + '=burst-time=1/1', + '=parent=%s' % parent_name, + '=total-queue=Djing_SFQ' )) - def remove_queue(self, user: AbonStruct, queue: AbonStruct = None) -> None: - if not isinstance(user, AbonStruct): + def remove_queue(self, queue: i_structs.SubnetQueue) -> None: + if not isinstance(queue, i_structs.SubnetQueue): raise TypeError - if queue is None: - queue = self.find_queue('uid%d' % user.uid) + if not queue.queue_id: + queue = self.find_queue(queue.name) if queue is not None: - queue_id = getattr(queue, 'queue_id') - if queue_id is not None: + if queue.queue_id: self._exec_cmd(( '/queue/simple/remove', - '=.id=%s' % queue_id + '=.id=%s' % queue.queue_id )) def remove_queue_range(self, q_ids: Iterable[str]): - self._exec_cmd(('/queue/simple/remove', '=numbers=' + ','.join(q_ids))) + ids = ','.join(q_ids) + if len(ids) > 1: + self._exec_cmd(('/queue/simple/remove', '=numbers=%s' % ids)) - def update_queue(self, user: AbonStruct, queue=None): - if not isinstance(user, AbonStruct): + def update_queue(self, queue: i_structs.SubnetQueue, parent_name: str): + if not isinstance(queue, i_structs.SubnetQueue): raise TypeError - if user.tariff is None: - return - if queue is None: - queue = self.find_queue('uid%d' % user.uid) + if not queue.queue_id: + queue = self.find_queue(queue.name) if queue is None: - return self.add_queue(user) + return self.add_queue(queue, parent_name) else: - mk_id = getattr(queue, 'queue_id') cmd = [ '/queue/simple/set', - '=name=uid%d' % user.uid, - '=max-limit=%.3fM/%.3fM' % (user.tariff.speedOut, user.tariff.speedIn), - # FIXME: тут в разных версиях прошивки микротика или =target-addresses или =target - '=target=%s' % user.ip, + '=name=%s' % queue.name, + '=max-limit=%.3fM/%.3fM' % queue.max_limit, + # FIXME: тут в разных версиях прошивки микротика + # или =target-addresses или =target + '=target=%s' % queue.network, '=queue=Djing_SFQ/Djing_SFQ', + '=parent=%s' % parent_name, '=burst-time=1/1' ] - if mk_id is not None: - cmd.insert(1, '=.id=%s' % mk_id) + if queue.queue_id: + cmd.insert(1, '=.id=%s' % queue.queue_id) r = self._exec_cmd(cmd) return r @@ -323,13 +336,13 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs # Ip->firewall->address list ################################################# - def add_ip(self, list_name: str, ip): - if not issubclass(ip.__class__, _BaseAddress): + def add_ip(self, list_name: str, net): + if not issubclass(net.__class__, _BaseNetwork): raise TypeError commands = ( '/ip/firewall/address-list/add', '=list=%s' % list_name, - '=address=%s' % ip + '=address=%s' % net ) return self._exec_cmd(commands) @@ -345,102 +358,67 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs '=numbers=%s' % ','.join(ip_firewall_ids) )) - def find_ip(self, ip, list_name: str): - if not issubclass(ip.__class__, _BaseAddress): + def find_ip(self, net, list_name: str): + if not issubclass(net.__class__, _BaseNetwork): raise TypeError r = self._exec_cmd(( '/ip/firewall/address-list/print', 'where', '?list=%s' % list_name, - '?address=%s' % ip + '?address=%s' % net )) return r.get('!re') - def read_ips_iter(self, list_name: str) -> Generator: - ips = self._exec_cmd_iter(( + def read_nets_iter(self, list_name: str) -> Generator: + nets = self._exec_cmd_iter(( '/ip/firewall/address-list/print', 'where', '?list=%s' % list_name, '?dynamic=no' )) - for dat in ips: - yield ip_address(dat.get('=address')), dat.get('=.id') + for dat in nets: + yield ip_network(dat.get('=address'), strict=False), dat.get( + '=.id') ################################################# # BaseTransmitter implementation ################################################# - def add_user_range(self, user_list: VectorAbon): - for usr in user_list: - self.add_user(usr) + def add_user_range(self, queue_list: i_structs.VectorQueue): + for q in queue_list: + self.add_user(q) - def remove_user_range(self, users: VectorAbon): - if not isinstance(users, (tuple, list, set)): + def remove_user_range(self, queues: i_structs.VectorQueue): + if not isinstance(queues, (tuple, list, set)): raise ValueError('*users* is used twice, generator does not fit') - queue_ids = (usr.queue_id for usr in users if usr is not None) + queue_ids = (q.queue_id for q in queues if q) self.remove_queue_range(queue_ids) - for user in users: - if isinstance(user, AbonStruct): - ip_list_entity = self.find_ip(user.ip, LIST_USERS_ALLOWED) + for q in queues: + if isinstance(q, i_structs.SubnetQueue): + ip_list_entity = self.find_ip(q.network, LIST_USERS_ALLOWED) if ip_list_entity: self.remove_ip(ip_list_entity.get('=.id')) - def add_user(self, user: AbonStruct, *args): - if user.tariff is None: - return - if not isinstance(user.tariff, TariffStruct): - raise TypeError + def add_user(self, queue: i_structs.SubnetQueue, parent_name=None, *args): try: - self.add_queue(user) - except NasFailedResult as e: + self.add_queue(queue, parent_name=parent_name) + except core.NasFailedResult as e: print('Error:', e) - ip = user.ip - if not issubclass(ip.__class__, _BaseAddress): + net = queue.network + if not issubclass(net.__class__, _BaseNetwork): raise TypeError try: - self.add_ip(LIST_USERS_ALLOWED, ip) - except NasFailedResult as e: + self.add_ip(LIST_USERS_ALLOWED, net) + except core.NasFailedResult as e: print('Error:', e) - def remove_user(self, user: AbonStruct): - self.remove_queue(user) - r = self.find_ip(user.ip, LIST_USERS_ALLOWED) + def remove_user(self, queue: i_structs.SubnetQueue): + self.remove_queue(queue) + r = self.find_ip(queue.network, LIST_USERS_ALLOWED) ip_id = r.get('=.id') self.remove_ip(ip_id) - def update_user(self, user: AbonStruct, *args): - # queue is instance of AbonStruct - queue = self.find_queue('uid%d' % user.uid) - ip = user.ip - - if not issubclass(ip.__class__, _BaseAddress): - raise TypeError - nas_ip = self.find_ip(ip, LIST_USERS_ALLOWED) - if user.is_access: - if nas_ip is None: - self.add_ip(LIST_USERS_ALLOWED, ip) - else: - # если не активен - то и обновлять не надо - # но и выключить на всяк случай надо, а то вдруг был включён - if nas_ip: - # и если найден был - то удалим ip из разрешённых - self.remove_ip(nas_ip.get('=.id')) - if queue is not None: - self.remove_queue(user, queue) - queue = None - - # если нет услуги то её не должно быть и в nas - if user.tariff is None: - if queue is not None: - self.remove_queue(user, queue) - return - if not user.is_access: - return - - # Проверяем шейпер - if queue is None: - self.add_queue(user) - return - if queue != user: - self.update_queue(user, queue) + def update_user(self, queue: i_structs.SubnetQueue, parent_name=None, + *args): + self.update_queue(queue, parent_name) def ping(self, host, count=10) -> Optional[Tuple[int, int]]: r = self._exec_cmd(( @@ -451,7 +429,8 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs return interface = r['!re'].get('=interface') r = self._exec_cmd(( - '/ping', '=address=%s' % host, '=arp-ping=yes', '=interval=100ms', '=count=%d' % count, + '/ping', '=address=%s' % host, '=arp-ping=yes', '=interval=100ms', + '=count=%d' % count, '=interface=%s' % interface )) res = r.get('!re') @@ -459,22 +438,83 @@ class MikrotikTransmitter(BaseTransmitter, ApiRos, metaclass=type('_ABC_Lazy_mcs received, sent = int(res.get('=received')), int(res.get('=sent')) return received, sent - def add_tariff_range(self, tariff_list: VectorTariff): - pass + def read_users(self) -> i_structs.VectorQueue: + return self.read_queue_iter() + + @staticmethod + def _build_db_queues(users_from_db: Iterator[Any]) -> Generator: + # Корневая очередь + # FIXME: Корневую очередь надо брать откуда-то + root_queue = i_structs.SubnetQueue( + name='queue-root', + network='10.0.0.0/8', + max_limit=2048, + queue_type=i_structs.SubnetQueue.QUEUE_ROOT + ) - def remove_tariff_range(self, tariff_list: VectorTariff): - pass + # выберем структуры подсетей + db_subnet_queues = (i_structs.SubnetQueue( + name="net_%s" % db_net.network, + network=db_net.get_network(), + max_limit=float(db_net.speed), + queue_type=i_structs.SubnetQueue.QUEUE_SUBNET + ) for db_net in NetworkModel.objects.all().iterator()) + + queues_struct_gen = ( + ab.build_agent_struct() for ab in users_from_db + if ab is not None and ab.is_access() + ) - def add_tariff(self, tariff: TariffStruct): - pass + r = [q for q in queues_struct_gen if q is not None] + r.insert(0, root_queue) + r.extend(db_subnet_queues) + return r, root_queue - def update_tariff(self, tariff: TariffStruct): - pass + def _queues_diff(self, users_from_db: Iterator): + queues_from_db, root_queue = self._build_db_queues(users_from_db) + queues_from_gw = tuple(self.read_queue_iter()) - def remove_tariff(self, tid: int): - pass + # TODO: надо чтоб корневая очередь тоже создавалась - def read_users(self) -> VectorAbon: - all_ips = set(ip for ip, mkid in self.read_ips_iter(LIST_USERS_ALLOWED)) - queues = (q for q in self.read_queue_iter() if all_ips.issuperset(q.ips)) - return queues + db_queues_subnets = ( + q for q in queues_from_db + if q.queue_type == i_structs.SubnetQueue.QUEUE_SUBNET + ) + gw_queues_subnets = tuple( + q for q in queues_from_gw + if q.queue_type == i_structs.SubnetQueue.QUEUE_SUBNET + ) + subnets_for_add, subnets_for_del = core.diff_set( + set(db_queues_subnets), set(gw_queues_subnets)) + + self.remove_queue_range( + (q.queue_id for q in subnets_for_del) + ) + for q in subnets_for_add: + self.add_queue(q, parent_name=root_queue.name) + del subnets_for_add, subnets_for_del + + db_queue_users = ( + q for q in queues_from_db + if q.queue_type == i_structs.SubnetQueue.QUEUE_LEAF + ) + gw_queue_users = ( + q for q in queues_from_gw + if q.queue_type == i_structs.SubnetQueue.QUEUE_LEAF + ) + user_q_for_add, user_q_for_del = core.diff_set(set(db_queue_users), + set(gw_queue_users)) + + self.remove_queue_range( + (q.queue_id for q in user_q_for_del) + ) + for q in user_q_for_add: + find_filter = filter( + lambda qe: qe.network.overlaps(q.network), + gw_queues_subnets + ) + parent_subnet = next(find_filter, root_queue) + self.add_queue(q, parent_name=parent_subnet.name) + + def sync_nas(self, users_from_db: Iterator): + self._queues_diff(users_from_db) diff --git a/nas_app/nas_managers/structs.py b/nas_app/nas_managers/structs.py index 69b3a33..7fa105f 100644 --- a/nas_app/nas_managers/structs.py +++ b/nas_app/nas_managers/structs.py @@ -1,5 +1,5 @@ from abc import ABCMeta -from ipaddress import ip_address, _BaseAddress +from ipaddress import ip_network, _BaseNetwork from typing import Iterable @@ -7,68 +7,76 @@ class BaseStruct(object, metaclass=ABCMeta): __slots__ = () -# Как обслуживается абонент -class TariffStruct(BaseStruct): - __slots__ = ('tid', 'speedIn', 'speedOut') +class SubnetQueue(BaseStruct): + __slots__ = ('name', '_net', '_max_limit', '_queue_type', + 'is_access', 'queue_id') - def __init__(self, tariff_id=0, speed_in=None, speed_out=None): - self.tid = int(tariff_id) - self.speedIn = speed_in or 0 - self.speedOut = speed_out or 0 + # Queue types + QUEUE_UNKNOWN = 0 + QUEUE_ROOT = 1 + QUEUE_SUBNET = 2 + QUEUE_LEAF = 3 - # Yes, if all variables is zeroed - def is_empty(self): - return self.tid == 0 and self.speedIn == 0 and self.speedOut == 0 - - def __eq__(self, other): - # не сравниваем id, т.к. тарифы с одинаковыми скоростями для NAS одинаковы - # Да и иногда не удобно доставать из nas id тарифы из базы - return self.speedIn == other.speedIn and self.speedOut == other.speedOut - - def __str__(self): - return "Id=%d, speedIn=%.2f, speedOut=%.2f" % (self.tid, self.speedIn, self.speedOut) + def __init__(self, name: str, network, max_limit=0.0, + queue_type=QUEUE_UNKNOWN, is_access=True, queue_id=None): + super().__init__() + self.name = name + self.network = network + self.max_limit = max_limit + self.queue_type = queue_type + self.is_access = is_access + self.queue_id = queue_id + + def get_max_limit(self): + return self._max_limit + + def set_max_limit(self, v): + if isinstance(v, tuple): + self._max_limit = v + elif isinstance(v, str): + s_in, s_out = v.split('/') + self._max_limit = float(s_in), float(s_out) + elif isinstance(v, (int, float)): + sp = float(v) + self._max_limit = sp, sp + else: + raise ValueError('Unexpected format for max_limit') - # нужно чтоб хеши тарифов In10,Out20 и In20,Out10 были разными - # поэтому сначала float->str и потом хеш - def __hash__(self): - return hash(str(self.speedIn) + str(self.speedOut)) + max_limit = property(get_max_limit, set_max_limit) + def get_network(self): + return self._net -# Abon from database -class AbonStruct(BaseStruct): - __slots__ = ('uid', '_ip', 'tariff', 'is_access', 'queue_id') + def set_network(self, v): + if isinstance(v, (str, int)): + self._net = ip_network(v, strict=False) + elif issubclass(v.__class__, _BaseNetwork): + self._net = v + else: + raise ValueError('Unexpected format for network') - def __init__(self, uid=0, ip=None, tariff=None, is_access=True): - self.uid = int(uid or 0) - self._ip = ip - self.tariff = tariff - self.is_access = is_access - self.queue_id = 0 + network = property(get_network, set_network) - def get_ip(self): - return self._ip + def get_queue_type(self): + return self._queue_type - def set_ip(self, v): - if issubclass(v.__class__, _BaseAddress): - self._ip = v - else: - self._ip = ip_address(v) + def set_queue_type(self, v): + if not isinstance(v, int): + raise ValueError('queue_type must be int') + if v < self.QUEUE_UNKNOWN or v > self.QUEUE_LEAF: + raise IndexError('queue_type out of range') + self._queue_type = v - ip = property(get_ip, set_ip, doc='Ip address') + queue_type = property(get_queue_type, set_queue_type) def __eq__(self, other): - if not isinstance(other, AbonStruct): - raise TypeError - r = self.uid == other.uid and self._ip == other._ip - r = r and self.tariff == other.tariff - return r - - def __str__(self): - return "uid=%d, ip=[%s], tariff=%s" % (self.uid, self._ip, self.tariff or '') + return self.network == other.network and self.max_limit == other.max_limit def __hash__(self): - return hash(hash(self._ip) + hash(self.tariff) if self.tariff is not None else 0) + return hash(str(self.max_limit) + str(self.network)) + + def __repr__(self): + return "net %s" % self.network -VectorAbon = Iterable[AbonStruct] -VectorTariff = Iterable[TariffStruct] +VectorQueue = Iterable[SubnetQueue] diff --git a/nas_app/views.py b/nas_app/views.py index ad0fbf4..1713127 100644 --- a/nas_app/views.py +++ b/nas_app/views.py @@ -34,8 +34,8 @@ class NasCreateView(CreateView): def form_valid(self, form): r = super(NasCreateView, self).form_valid(form) assign_perm("nas_app.change_nasmodel", self.request.user, self.object) - assign_perm("nas_app.view_nas", self.request.user, self.object) - assign_perm("nas_app.delete_nas", self.request.user, self.object) + assign_perm("nas_app.view_nasmodel", self.request.user, self.object) + assign_perm("nas_app.delete_nasmodel", self.request.user, self.object) self.request.user.log(self.request.META, 'cnas', '"%(title)s", %(ip)s, %(type)s' % { 'title': self.object.title, 'ip': self.object.ip_address,