initial
This commit is contained in:
0
mikrotik/__init__.py
Normal file
0
mikrotik/__init__.py
Normal file
56
mikrotik/admin.py
Normal file
56
mikrotik/admin.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from lib.decorators import readonly
|
||||
from lib.mikrotik import sync_from_mikrotik as _sync_from_mikrotik
|
||||
from mikrotik.models import DNSStatic, IPAddress, IPDHCPLease
|
||||
|
||||
|
||||
@readonly
|
||||
def sync_dns_static_from_mikrotik(*args, **kwargs):
|
||||
_sync_from_mikrotik(DNSStatic)
|
||||
|
||||
|
||||
@readonly
|
||||
def sync_ipaddress_from_mikrotik(*args, **kwargs):
|
||||
_sync_from_mikrotik(IPAddress)
|
||||
|
||||
|
||||
@readonly
|
||||
def sync_ipdhcplease_from_mikrotik(*args, **kwargs):
|
||||
_sync_from_mikrotik(IPDHCPLease)
|
||||
|
||||
def sync_to_mikrotik(_, request, queryset):
|
||||
for i in queryset:
|
||||
i.sync_to_router()
|
||||
|
||||
class MikrotikModelMixinAdmin(admin.ModelAdmin):
|
||||
def change_view(self, request, object_id, form_url='', extra_context=None):
|
||||
self.get_object(request, object_id).sync_from_router()
|
||||
return super().change_view(
|
||||
request, object_id, form_url, extra_context=extra_context or {'show_save_and_continue': False}
|
||||
)
|
||||
|
||||
|
||||
@admin.register(DNSStatic)
|
||||
class DNSStaticAdmin(admin.ModelAdmin):
|
||||
actions = [sync_dns_static_from_mikrotik, sync_to_mikrotik]
|
||||
list_display = ('name', 'regexp', 'address','disabled', 'comment', 'id')
|
||||
list_filter = ('disabled',('id', admin.EmptyFieldListFilter), ('name', admin.EmptyFieldListFilter), ('address', admin.EmptyFieldListFilter), ('regexp', admin.EmptyFieldListFilter),)
|
||||
search_fields = ('name', 'address','regexp')
|
||||
|
||||
|
||||
|
||||
@admin.register(IPAddress)
|
||||
class IPAddressAdmin(admin.ModelAdmin):
|
||||
actions = [sync_ipaddress_from_mikrotik, sync_to_mikrotik]
|
||||
list_display = ('address','network', 'disabled', 'comment', 'id')
|
||||
list_filter = ('disabled',('id', admin.EmptyFieldListFilter), ('address', admin.EmptyFieldListFilter), ('network', admin.EmptyFieldListFilter),)
|
||||
search_fields = ('address', 'network', 'comment')
|
||||
|
||||
|
||||
@admin.register(IPDHCPLease)
|
||||
class IPDHCPLeaseAdmin(admin.ModelAdmin):
|
||||
actions = [sync_ipdhcplease_from_mikrotik, sync_to_mikrotik]
|
||||
list_display = ('address', 'mac_address', 'hostname', 'disabled', 'dynamic', 'comment', 'status', 'id')
|
||||
list_filter = ('disabled', 'dynamic', 'status', ('id', admin.EmptyFieldListFilter), ('address', admin.EmptyFieldListFilter), ('mac_address', admin.EmptyFieldListFilter), ('hostname', admin.EmptyFieldListFilter), ('comment', admin.EmptyFieldListFilter),)
|
||||
search_fields = ('address', 'mac_address', 'hostname')
|
||||
6
mikrotik/apps.py
Normal file
6
mikrotik/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MikrotikConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mikrotik'
|
||||
83
mikrotik/migrations/0001_initial.py
Normal file
83
mikrotik/migrations/0001_initial.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-07 11:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DNSStatic',
|
||||
fields=[
|
||||
('internal_id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('disabled', models.CharField(blank=True, choices=[('false', 'No'), ('true', 'Yes')], default='false', max_length=10, null=True)),
|
||||
('comment', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('id', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('name', models.CharField(blank=True, max_length=150, null=True, unique=True)),
|
||||
('address', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('ttl', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('dynamic', models.CharField(blank=True, default='', editable=False, max_length=150, null=True)),
|
||||
('regexp', models.CharField(blank=True, max_length=150, null=True, unique=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IPAddress',
|
||||
fields=[
|
||||
('internal_id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('disabled', models.CharField(blank=True, choices=[('false', 'No'), ('true', 'Yes')], default='false', max_length=10, null=True)),
|
||||
('comment', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('id', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('address', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('network', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('interface', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('actual_interface', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('invalid', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('dynamic', models.CharField(blank=True, default='', editable=False, max_length=150, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IPDHCPLease',
|
||||
fields=[
|
||||
('internal_id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('disabled', models.CharField(blank=True, choices=[('false', 'No'), ('true', 'Yes')], default='false', max_length=10, null=True)),
|
||||
('comment', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('id', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('address', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('mac_address', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('client_id', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('hostname', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('valid_until', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('dynamic', models.CharField(blank=True, default='', editable=False, max_length=150, null=True)),
|
||||
('blocked', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('active_client_id', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('active_mac_address', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('expires_after', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('age', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('active_server', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('active_address', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('host_name', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('radius', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('last_seen', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('dhcp_option', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('status', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('server', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('address_lists', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('always_broadcast', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
('lease_time', models.CharField(blank=True, default='', max_length=150, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-08 12:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mikrotik', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='dnsstatic',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, default='', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipaddress',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, default='', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ipdhcplease',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, default='', null=True),
|
||||
),
|
||||
]
|
||||
0
mikrotik/migrations/__init__.py
Normal file
0
mikrotik/migrations/__init__.py
Normal file
215
mikrotik/models.py
Normal file
215
mikrotik/models.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from lib.db import TaskAwareModelMixin
|
||||
from lib.decorators import skip_signal
|
||||
from lib.mikrotik import MikrotikModelMixin, is_local_ip
|
||||
|
||||
|
||||
class DNSStatic(MikrotikModelMixin, TaskAwareModelMixin):
|
||||
id = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
name = models.CharField(max_length=150, null=True, blank=True, unique=True, )
|
||||
address = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
ttl = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
dynamic = models.CharField(max_length=150, null=True, blank=True, default='', editable=False)
|
||||
regexp = models.CharField(max_length=150, null=True, blank=True, unique=True, )
|
||||
|
||||
# lease = models.ForeignKey('IPDHCPLease', on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='dns_statics', name)
|
||||
|
||||
@property
|
||||
def unique_on_router(self):
|
||||
return ['address', ('name', 'regexp')]
|
||||
|
||||
@property
|
||||
def get_self_params(self):
|
||||
return {f:getattr(self, f) for f in ['address', 'name', 'regexp', 'comment'] if getattr(self, f, None)}
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.address} - {self.name or self.regexp}'
|
||||
|
||||
@property
|
||||
def no_mikrotik_props(self):
|
||||
return super().no_mikrotik_props + ['ttl']
|
||||
|
||||
@property
|
||||
def router_base(self):
|
||||
return '/ip/dns/static'
|
||||
|
||||
@cached_property
|
||||
def router_list(self):
|
||||
ret = []
|
||||
for r in self.router_get_all:
|
||||
if is_local_ip(r['address']):
|
||||
ret.append(r)
|
||||
self.response = ret
|
||||
return self.response
|
||||
|
||||
|
||||
class IPAddress(MikrotikModelMixin, TaskAwareModelMixin):
|
||||
id = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
address = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
network = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
interface = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
actual_interface = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
invalid = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
dynamic = models.CharField(max_length=150, null=True, blank=True, default='', editable=False)
|
||||
|
||||
@property
|
||||
def unique_on_router(self):
|
||||
return ['address', 'network', 'interface']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.address} - {self.comment}'
|
||||
|
||||
@property
|
||||
def router_base(self):
|
||||
return '/ip/address'
|
||||
|
||||
@property
|
||||
def no_mikrotik_props(self):
|
||||
return super().no_mikrotik_props + [
|
||||
'actual_interface',
|
||||
'invalid',
|
||||
]
|
||||
|
||||
@property
|
||||
def get_next_ip(self):
|
||||
# sync_from_mikrotik(IPDHCPLease)
|
||||
net = '.'.join(self.network.split('.')[:-1]) + '.'
|
||||
used = sorted(IPDHCPLease.objects.filter(address__startswith=net).values_list('address', flat=True),
|
||||
reverse=True)
|
||||
next32 = 1
|
||||
if used:
|
||||
next32 += int(used[0].split('.')[-1])
|
||||
return f'{net}{next32}'
|
||||
|
||||
@cached_property
|
||||
def router_list(self):
|
||||
""" { "id": "*XXX", "address": "X.X.X.X/XX",
|
||||
"network": "X.X.X.X", "interface": "<>",
|
||||
"actual-interface": "", "invalid": "false",
|
||||
"dynamic": "true", "disabled": "false" } """
|
||||
ret = []
|
||||
for r in self.router_get_all:
|
||||
if all([
|
||||
r['invalid'] == 'false',
|
||||
r['disabled'] == 'false',
|
||||
'container' in r.get('comment', '').lower(),
|
||||
is_local_ip(r['address']),
|
||||
r['address'].endswith('.1/24'),
|
||||
]):
|
||||
ret.append(r)
|
||||
return ret
|
||||
|
||||
|
||||
class IPDHCPLease(MikrotikModelMixin, TaskAwareModelMixin):
|
||||
id = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
address = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
mac_address = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
client_id = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
hostname = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
valid_until = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
dynamic = models.CharField(max_length=150, null=True, blank=True, default='', editable=False)
|
||||
blocked = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
active_client_id = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
active_mac_address = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
expires_after = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
age = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
active_server = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
active_address = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
host_name = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
radius = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
last_seen = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
dhcp_option = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
status = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
server = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
address_lists = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
always_broadcast = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
lease_time = models.CharField(max_length=150, null=True, blank=True, default='')
|
||||
|
||||
@property
|
||||
def unique_on_router(self):
|
||||
return ['address', 'nac_address']
|
||||
|
||||
|
||||
@property
|
||||
def mikrotik_send_params(self):
|
||||
return {
|
||||
'address': self.address,
|
||||
'mac-address': self.mac_address,
|
||||
'comment': self.comment,
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.mac_address = self.mac_address.upper() if self.mac_address else ''
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def network_24(self):
|
||||
return '.'.join(self.address.split('.')[:-1]) + '.'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.address} - {self.mac_address} - {self.status}'
|
||||
|
||||
@property
|
||||
def router_base(self):
|
||||
return '/ip/dhcp-server/lease'
|
||||
|
||||
@cached_property
|
||||
def router_list(self):
|
||||
return self.router_get_all
|
||||
|
||||
@property
|
||||
def no_mikrotik_props(self):
|
||||
return super().no_mikrotik_props + [
|
||||
'active_client_id',
|
||||
'active_mac_address',
|
||||
'expires_after',
|
||||
'age',
|
||||
'active_server',
|
||||
'active_address',
|
||||
'hostname',
|
||||
'host_name',
|
||||
'radius',
|
||||
'last_seen',
|
||||
'status',
|
||||
'lease_time',
|
||||
'server',
|
||||
]
|
||||
|
||||
|
||||
for cl in [IPAddress, IPDHCPLease, DNSStatic]:
|
||||
@receiver(pre_save, sender=cl)
|
||||
def send_before_save(sender, instance: MikrotikModelMixin, **kwargs):
|
||||
if instance._state.adding:
|
||||
logging.info(f'Created {instance} via pre_save event - do nothing')
|
||||
return instance
|
||||
try:
|
||||
response = instance.sync_to_router()
|
||||
logging.debug(f'Update {instance} to router with {response}')
|
||||
except Exception as e:
|
||||
logging.error(f'Error while updating {instance} to router: {e}')
|
||||
return instance
|
||||
|
||||
@receiver(post_save, sender=cl)
|
||||
@skip_signal(signaltype='post_save')
|
||||
def send_after_save(sender, instance: MikrotikModelMixin, created, **kwargs):
|
||||
if created:
|
||||
logging.info(f'Created {instance} via post_save event - sync once')
|
||||
instance.sync_to_router()
|
||||
return instance
|
||||
|
||||
|
||||
# @skip_signal(signaltype='pre_delete')
|
||||
@receiver(pre_delete, sender=cl)
|
||||
def send_before_delete(sender, instance, **kwargs):
|
||||
try:
|
||||
response = instance.delete_from_router()
|
||||
logging.debug(f'Deleted {instance} from router with {response}')
|
||||
except Exception as e:
|
||||
logging.error(f'Error while deleting {instance} from router: {e}')
|
||||
return instance
|
||||
3
mikrotik/tests.py
Normal file
3
mikrotik/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
mikrotik/views.py
Normal file
3
mikrotik/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user