fetch and store router information

This commit is contained in:
Eduardo Silva 2025-04-24 15:56:24 -03:00
parent 130bb25dca
commit e40eb2fdca
10 changed files with 325 additions and 3 deletions

View file

@ -1,5 +1,6 @@
*/5 * * * * root sleep 0 ; /usr/bin/curl -s http://routerfleet:8001/cron/generate_backup_schedule/ >> /var/log/cron.log 2>&1 */5 * * * * root sleep 0 ; /usr/bin/curl -s http://routerfleet:8001/cron/generate_backup_schedule/ >> /var/log/cron.log 2>&1
* * * * * root sleep 5 ; /usr/bin/curl -s http://routerfleet:8001/cron/create_backup_tasks/ >> /var/log/cron.log 2>&1 * * * * * root sleep 5 ; /usr/bin/curl -s http://routerfleet:8001/cron/create_backup_tasks/ >> /var/log/cron.log 2>&1
* * * * * root sleep 10; /usr/bin/curl -s http://routerfleet:8001/cron/update_router_information/ >> /var/log/cron.log 2>&1
*/10 * * * * root sleep 20; /usr/bin/curl -s http://routerfleet:8001/cron/housekeeping/ >> /var/log/cron.log 2>&1 */10 * * * * root sleep 20; /usr/bin/curl -s http://routerfleet:8001/cron/housekeeping/ >> /var/log/cron.log 2>&1
* * * * * root sleep 40; /usr/bin/curl -s http://routerfleet:8001/cron/perform_backup_tasks/ >> /var/log/cron.log 2>&1 * * * * * root sleep 40; /usr/bin/curl -s http://routerfleet:8001/cron/perform_backup_tasks/ >> /var/log/cron.log 2>&1
* * * * * root sleep 50; /usr/bin/curl -s http://routerfleet:8001/cron/check_updates/ >> /var/log/cron.log 2>&1 * * * * * root sleep 50; /usr/bin/curl -s http://routerfleet:8001/cron/check_updates/ >> /var/log/cron.log 2>&1

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-04-23 12:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('import_tool', '0005_importtask_import_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='importtask',
name='router_type',
field=models.CharField(choices=[('monitoring', 'Monitoring Only'), ('routeros', 'Mikrotik (RouterOS)'), ('routeros-branded', 'Mikrotik (Branded)'), ('openwrt', 'OpenWRT')], max_length=100),
),
]

View file

@ -1,5 +1,12 @@
from django.contrib import admin from django.contrib import admin
from .models import Router, SSHKey, RouterStatus, BackupSchedule from .models import Router, SSHKey, RouterStatus, BackupSchedule, RouterInformation
class RouterInformationAdmin(admin.ModelAdmin):
list_display = ('router', 'model_name', 'model_version', 'serial_number', 'os_version', 'firmware_version', 'architecture')
search_fields = ('router__name', 'model_name', 'model_version', 'serial_number', 'os_version', 'firmware_version', 'architecture')
list_filter = ('router__name', 'model_name', 'model_version', 'serial_number', 'os_version', 'firmware_version', 'architecture')
admin.site.register(RouterInformation, RouterInformationAdmin)
class RouterAdmin(admin.ModelAdmin): class RouterAdmin(admin.ModelAdmin):

View file

@ -0,0 +1,44 @@
# Generated by Django 5.2 on 2025-04-23 12:24
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('router_manager', '0016_router_port'),
]
operations = [
migrations.AlterField(
model_name='router',
name='router_type',
field=models.CharField(choices=[('monitoring', 'Monitoring Only'), ('routeros', 'Mikrotik (RouterOS)'), ('routeros-branded', 'Mikrotik (Branded)'), ('openwrt', 'OpenWRT')], max_length=100),
),
migrations.CreateModel(
name='RouterInformation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('success', models.BooleanField(default=False)),
('error', models.BooleanField(default=False)),
('error_message', models.TextField(blank=True, null=True)),
('retry_count', models.IntegerField(default=0)),
('next_retry', models.DateTimeField(blank=True, null=True)),
('last_retrieval', models.DateTimeField(blank=True, null=True)),
('host_id', models.CharField(blank=True, max_length=100, null=True)),
('model_name', models.CharField(blank=True, max_length=100, null=True)),
('model_version', models.CharField(blank=True, max_length=100, null=True)),
('serial_number', models.CharField(blank=True, max_length=100, null=True)),
('os_version', models.CharField(blank=True, max_length=100, null=True)),
('firmware_version', models.CharField(blank=True, max_length=100, null=True)),
('architecture', models.CharField(blank=True, max_length=100, null=True)),
('json_data', models.TextField(blank=True, null=True)),
('updated', models.DateTimeField(auto_now=True)),
('created', models.DateTimeField(auto_now_add=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('router', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='router_manager.router')),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-04-24 17:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('router_manager', '0017_alter_router_router_type_routerinformation'),
]
operations = [
migrations.AddField(
model_name='routerinformation',
name='cpu',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2 on 2025-04-24 17:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('router_manager', '0018_routerinformation_cpu'),
]
operations = [
migrations.RemoveField(
model_name='routerinformation',
name='host_id',
),
]

View file

@ -80,3 +80,31 @@ class BackupSchedule(models.Model):
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4) uuid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4)
class RouterInformation(models.Model):
router = models.OneToOneField(Router, on_delete=models.CASCADE)
success = models.BooleanField(default=False)
error = models.BooleanField(default=False)
error_message = models.TextField(blank=True, null=True)
retry_count = models.IntegerField(default=0)
next_retry = models.DateTimeField(blank=True, null=True)
last_retrieval = models.DateTimeField(blank=True, null=True)
model_name = models.CharField(max_length=100, null=True, blank=True)
model_version = models.CharField(max_length=100, null=True, blank=True)
serial_number = models.CharField(max_length=100, null=True, blank=True)
os_version = models.CharField(max_length=100, null=True, blank=True)
firmware_version = models.CharField(max_length=100, null=True, blank=True)
architecture = models.CharField(max_length=100, null=True, blank=True)
cpu = models.CharField(max_length=100, null=True, blank=True)
json_data = models.TextField(null=True, blank=True)
updated = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4)
def __str__(self):
return str(self.router)

View file

@ -8,9 +8,11 @@ from django.utils import timezone
from backup.models import BackupProfile from backup.models import BackupProfile
from backup_data.models import RouterBackup from backup_data.models import RouterBackup
from routerfleet_tools.models import WebadminSettings from routerfleet_tools.models import WebadminSettings
from routerlib.router_functions import update_router_information
from user_manager.models import UserAcl from user_manager.models import UserAcl
from .forms import RouterForm, RouterGroupForm, SSHKeyForm from .forms import RouterForm, RouterGroupForm, SSHKeyForm
from .models import Router, RouterGroup, RouterStatus, SSHKey, BackupSchedule from .models import Router, RouterGroup, RouterInformation, RouterStatus, SSHKey, BackupSchedule
from django.conf import settings
@login_required @login_required
@ -263,3 +265,28 @@ def view_create_instant_backup_multiple_routers(request):
return JsonResponse({'results': results}) return JsonResponse({'results': results})
return JsonResponse({'error': 'Invalid request method.'}, status=405) return JsonResponse({'error': 'Invalid request method.'}, status=405)
def view_cron_update_router_information(request):
data = {'status': 'success'}
refresh_interval = 24 #hours
router_list = Router.objects.filter(enabled=True).exclude(router_type='monitoring').exclude(routerstatus__status_online=False)
router = router_list.filter(routerinformation__isnull=True).first()
if not router:
router = router_list.filter(routerinformation__next_retry__lt=timezone.now()).first()
if not router:
router = router_list.filter(routerinformation__last_retrieval__isnull=True).first()
if not router:
router = router_list.filter(routerinformation__last_retrieval__lt=timezone.now() - timezone.timedelta(hours=refresh_interval)).first()
if router:
router_information, created = RouterInformation.objects.get_or_create(router=router)
success, error_message = update_router_information(router_information)
if not success:
data['status'] = 'error'
data['message'] = 'Failed to update router'
else:
data['message'] = 'No routers need update'
return JsonResponse(data)

View file

@ -6,7 +6,7 @@ from dashboard.views import view_dashboard, view_status,backup_statistics_data,r
from integration_manager.views import view_wireguard_webadmin_launcher, view_manage_wireguard_integration, view_launch_wireguard_webadmin from integration_manager.views import view_wireguard_webadmin_launcher, view_manage_wireguard_integration, view_launch_wireguard_webadmin
from user_manager.views import view_manage_user, view_user_list from user_manager.views import view_manage_user, view_user_list
from accounts.views import view_login, view_logout, view_create_first_user from accounts.views import view_login, view_logout, view_create_first_user
from router_manager.views import view_create_instant_backup_multiple_routers, view_router_list, view_manage_router, view_router_group_list, view_ssh_key_list, view_manage_router_group, view_manage_sshkey, view_router_details, view_create_instant_backup_task, view_router_availability from router_manager.views import view_create_instant_backup_multiple_routers, view_router_list, view_manage_router, view_router_group_list, view_ssh_key_list, view_manage_router_group, view_manage_sshkey, view_router_details, view_create_instant_backup_task, view_router_availability, view_cron_update_router_information
from backup.views import view_backup_profile_list, view_manage_backup_profile, view_backup_list, view_backup_details, view_debug_run_backups, view_compare_backups, view_backup_download, view_backup_delete from backup.views import view_backup_profile_list, view_manage_backup_profile, view_backup_list, view_backup_details, view_debug_run_backups, view_compare_backups, view_backup_download, view_backup_delete
from monitoring.views import view_export_router_list, view_update_router_status, view_router_config_timestamp, view_router_last_status_change from monitoring.views import view_export_router_list, view_update_router_status, view_router_config_timestamp, view_router_last_status_change
from backup_data.views import view_generate_backup_schedule, view_create_backup_tasks, view_perform_backup_tasks, view_housekeeping from backup_data.views import view_generate_backup_schedule, view_create_backup_tasks, view_perform_backup_tasks, view_housekeeping
@ -58,6 +58,7 @@ urlpatterns = [
path('cron/perform_backup_tasks/', view_perform_backup_tasks, name='perform_backup_tasks'), path('cron/perform_backup_tasks/', view_perform_backup_tasks, name='perform_backup_tasks'),
path('cron/housekeeping/', view_housekeeping, name='housekeeping'), path('cron/housekeeping/', view_housekeeping, name='housekeeping'),
path('cron/check_updates/', cron_check_updates, name='check_updates'), path('cron/check_updates/', cron_check_updates, name='check_updates'),
path('cron/update_router_information/', view_cron_update_router_information, name='update_router_information'),
path('cron/concatenate_notifications/', view_cron_concatenate_notifications, name='concatenate_notifications'), path('cron/concatenate_notifications/', view_cron_concatenate_notifications, name='concatenate_notifications'),
path('cron/send_messages/', view_cron_send_messages, name='send_messages'), path('cron/send_messages/', view_cron_send_messages, name='send_messages'),
path('cron/daily_reports/', view_cron_daily_reports, name='daily_reports'), path('cron/daily_reports/', view_cron_daily_reports, name='daily_reports'),

View file

@ -0,0 +1,161 @@
import json
import datetime
from django.utils import timezone
from router_manager.models import RouterInformation, Router
from routerlib.functions import connect_to_ssh
def _parse_routeros_key_value_output(output: str) -> dict:
"""
Parse lines like "key: value" into a dict.
Skip blank lines or lines starting with '[' (the prompt).
"""
data = {}
# Normalize and split
for raw_line in output.replace('\r', '').splitlines():
line = raw_line.strip()
# skip empty or prompt lines
if not line or line.startswith('['):
continue
if ':' not in line:
continue
key, val = line.split(':', 1)
data[key.strip()] = val.strip()
return data
def get_router_information(router_information: RouterInformation):
"""
Connect to the router, retrieve info, and store it in RouterInformation.
"""
router = router_information.router
field_max_length = 100
success = False
error_message = ''
try:
ssh = connect_to_ssh(router.address, router.port, router.username, router.password, router.ssh_key)
json_data = {}
if router.router_type in ('routeros', 'routeros-branded'):
for cmd in ['/system resource print', '/system routerboard print']:
stdin, stdout, stderr = ssh.exec_command(cmd)
raw = stdout.read().decode('utf-8', errors='ignore')
parsed = _parse_routeros_key_value_output(raw)
json_data[cmd] = parsed
rb = json_data['/system routerboard print']
sr = json_data['/system resource print']
if sr:
router_information.model_name = sr.get('board-name', '')[:field_max_length]
router_information.os_version = sr.get('version', '')[:field_max_length]
router_information.architecture = sr.get('architecture-name', '')[:field_max_length]
router_information.cpu = sr.get('cpu', '')[:field_max_length]
success = True
if rb:
router_information.model_version = rb.get('model', '')[:field_max_length]
router_information.serial_number = rb.get('serial-number', '')[:field_max_length]
router_information.firmware_version = rb.get('current-firmware', '')[:field_max_length]
success = True
if not success:
return False, 'Failed to retrieve router information'
elif router.router_type == 'openwrt':
stdin, stdout, stderr = ssh.exec_command('cat /etc/os-release')
osrel = {}
for line in stdout.read().decode('utf-8').splitlines():
if '=' in line:
k, v = line.split('=', 1)
osrel[k] = v.strip().strip('"')
json_data['cat /etc/os-release'] = osrel
# hostname
stdin, stdout, stderr = ssh.exec_command('uci get system.@system[0].hostname')
hostname = stdout.read().decode('utf-8').strip()
json_data['uci get system.@system[0].hostname'] = hostname
# architecture
stdin, stdout, stderr = ssh.exec_command('uname -m')
arch = stdout.read().decode('utf-8').strip()
json_data['uname -m'] = arch
# fallback serial (MAC of eth0)
stdin, stdout, stderr = ssh.exec_command('cat /sys/class/net/eth0/address')
mac = stdout.read().decode('utf-8').strip()
json_data['cat /sys/class/net/eth0/address'] = mac
if osrel:
router_information.model_name = osrel.get('OPENWRT_DEVICE_MODEL', '')[:field_max_length]
router_information.model_version = osrel.get('VERSION_ID', '')[:field_max_length]
router_information.serial_number = mac[:field_max_length]
router_information.os_version = osrel.get('VERSION', '')[:field_max_length]
router_information.firmware_version = osrel.get('OPENWRT_RELEASE', '')[:field_max_length]
router_information.architecture = arch[:field_max_length]
success = True
if not success:
return False, 'Failed to retrieve router information'
else:
return False, f"Router type not supported: {router.get_router_type_display()}"
if success:
router_information.success = True
router_information.error = False
router_information.retry_count = 0
router_information.next_retry = None
router_information.error_message = ''
router_information.last_retrieval = timezone.now()
router_information.json_data = json.dumps(json_data)
router_information.save()
except Exception as e:
success = False
error_message = str(e)
finally:
try:
ssh.close()
except:
pass
return success, error_message
def update_router_information(router_information: RouterInformation):
max_retry = 3
retry_minutes = 5
success = False
error_message = ''
if router_information.retry_count > max_retry:
router_information.error = True
router_information.success = False
router_information.next_retry = None
router_information.retry_count = 0
router_information.last_retrieval = timezone.now()
if router_information.error_message:
router_information.error_message += f"\nMax retries reached for {router_information.router.name}"
else:
router_information.error_message = f"Max retries reached for {router_information.router.name}"
router_information.save()
return False, router_information.error_message
try:
success, error_message = get_router_information(router_information)
except Exception as e:
success = False
error_message = f"Failed to update router information for {router_information.router.name}. Exception: {e}"
if not success:
router_information.error = True
router_information.success = False
router_information.next_retry = timezone.now() + datetime.timedelta(minutes=retry_minutes)
router_information.retry_count += 1
router_information.last_retrieval = timezone.now()
if error_message:
router_information.error_message = error_message
else:
router_information.error_message = f"Failed to update router information for {router_information.router.name}"
router_information.save()
return success, error_message