diff --git a/.gitignore b/.gitignore index 8fcb794..abd5cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ *.pyd routerfleet/production_settings.py .idea/ -db.sqlite3 \ No newline at end of file +db.sqlite3 +backups/ \ No newline at end of file diff --git a/backup/views.py b/backup/views.py index 7af05c0..533e5c7 100644 --- a/backup/views.py +++ b/backup/views.py @@ -1,6 +1,9 @@ from django.contrib.auth.decorators import login_required +from django.http import JsonResponse from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages + +from routerlib.backup_functions import perform_backup from .models import BackupProfile from .forms import BackupProfileForm from router_manager.models import Router @@ -77,4 +80,15 @@ def view_backup_details(request): 'backup': backup, 'page_title': 'Backup Details' } - return render(request, 'backup/backup_details.html', context) \ No newline at end of file + return render(request, 'backup/backup_details.html', context) + + +def view_debug_run_backups(request): + data = { + 'backup_count': 0, + } + for backup in RouterBackup.objects.filter(success=False, error=False): + data['backup_count'] += 1 + perform_backup(backup) + + return JsonResponse(data) \ No newline at end of file diff --git a/backup_data/migrations/0002_routerbackup_backup_binary_and_more.py b/backup_data/migrations/0002_routerbackup_backup_binary_and_more.py new file mode 100644 index 0000000..cf51692 --- /dev/null +++ b/backup_data/migrations/0002_routerbackup_backup_binary_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.3 on 2024-03-20 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('backup_data', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='routerbackup', + name='backup_binary', + field=models.FileField(blank=True, null=True, upload_to='backups/'), + ), + migrations.AddField( + model_name='routerbackup', + name='backup_pending_retrieval', + field=models.BooleanField(default=False), + ), + ] diff --git a/backup_data/models.py b/backup_data/models.py index b2b3d1b..f4fbcc7 100644 --- a/backup_data/models.py +++ b/backup_data/models.py @@ -7,6 +7,7 @@ class RouterBackup(models.Model): router = models.ForeignKey(Router, on_delete=models.CASCADE) success = models.BooleanField(default=False) error = models.BooleanField(default=False) + backup_pending_retrieval = 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) @@ -15,6 +16,7 @@ class RouterBackup(models.Model): queue_length = models.IntegerField(default=0) # Seconds finish_time = models.DateTimeField(blank=True, null=True) backup_text = models.TextField(blank=True, null=True) + backup_binary = models.FileField(upload_to='backups/', blank=True, null=True) updated = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) diff --git a/requirements.txt b/requirements.txt index d3ca6f3..36ffd66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,14 @@ asgiref==3.7.2 +bcrypt==4.1.2 +cffi==1.16.0 crispy-bootstrap4==2024.1 crispy-bootstrap5==2024.2 +cryptography==42.0.5 Django==5.0.3 django-crispy-forms==2.1 +paramiko==3.4.0 +pycparser==2.21 +PyNaCl==1.5.0 +scp==0.14.5 sqlparse==0.4.4 typing_extensions==4.10.0 diff --git a/router_manager/forms.py b/router_manager/forms.py index 7ac1c92..ef50c5f 100644 --- a/router_manager/forms.py +++ b/router_manager/forms.py @@ -2,6 +2,7 @@ from django import forms from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit, Row, Column, HTML from .models import Router, RouterGroup, SSHKey +from routerlib.functions import test_authentication import ipaddress import socket @@ -82,7 +83,15 @@ class RouterForm(forms.ModelForm): ipaddress.ip_address(address) except ValueError: raise forms.ValidationError('The address field must be a valid hostname or IP address.') - + test_authentication_success, test_authentication_message = test_authentication( + cleaned_data['router_type'], cleaned_data['address'], cleaned_data['username'], cleaned_data['password'], + cleaned_data['ssh_key'] + ) + if not test_authentication_success: + if test_authentication_message: + raise forms.ValidationError('Could not authenticate: ' + test_authentication_message) + else: + raise forms.ValidationError('Could not authenticate to the router. Please check the credentials and try again.') return cleaned_data diff --git a/routerfleet/urls.py b/routerfleet/urls.py index d7ad59d..dd975a0 100644 --- a/routerfleet/urls.py +++ b/routerfleet/urls.py @@ -4,11 +4,12 @@ from dashboard.views import view_dashboard, view_status from user_manager.views import view_manage_user, view_user_list from accounts.views import view_login, view_logout, view_create_first_user from router_manager.views import view_router_list, view_manage_router, view_router_group_list, view_ssh_key_list, view_manage_router_group, view_manage_sshkey -from backup.views import view_backup_profile_list, view_manage_backup_profile, view_backup_list, view_backup_details +from backup.views import view_backup_profile_list, view_manage_backup_profile, view_backup_list, view_backup_details, view_debug_run_backups urlpatterns = [ path('admin/', admin.site.urls), + path('debug/run_backups/', view_debug_run_backups, name='debug_run_backups'), path('', view_dashboard, name='dashboard'), path('status/', view_status, name='status'), path('user/list/', view_user_list, name='user_list'), diff --git a/routerlib/__init__.py b/routerlib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routerlib/admin.py b/routerlib/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/routerlib/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/routerlib/apps.py b/routerlib/apps.py new file mode 100644 index 0000000..b1d4607 --- /dev/null +++ b/routerlib/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RouterlibConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'routerlib' diff --git a/routerlib/backup_functions.py b/routerlib/backup_functions.py new file mode 100644 index 0000000..cdb6671 --- /dev/null +++ b/routerlib/backup_functions.py @@ -0,0 +1,150 @@ +import datetime +from django.utils import timezone +from backup_data.models import RouterBackup +import paramiko +import os +from scp import SCPClient +from django.core.files.base import ContentFile + + +def perform_backup(router_backup: RouterBackup): + if router_backup.success or router_backup.error: + return + if not router_backup.router.backup_profile: + router_backup.error = True + router_backup.error_message = "No backup profile assigned" + router_backup.save() + return + if router_backup.retry_count > router_backup.router.backup_profile.max_retry: + router_backup.error = True + router_backup.save() + return + + if router_backup.backup_pending_retrieval: + backup_success, error_message = retrieve_backup(router_backup) + if error_message: + handle_backup_failure(router_backup, error_message) + return + + if backup_success: + clean_up_backup_files(router_backup) + if router_backup.schedule_time: + start_time = router_backup.schedule_time + else: + start_time = router_backup.created + router_backup.queue_length = (timezone.now() - start_time).seconds + router_backup.finish_time = timezone.now() + router_backup.backup_pending_retrieval = False + router_backup.error_message = '' + router_backup.success = True + router_backup.save() + else: + handle_backup_failure(router_backup, error_message) + else: + backup_success, backup_files, error_message = execute_backup(router_backup) + if backup_success: + router_backup.backup_pending_retrieval = True + router_backup.error_message = '' + router_backup.retry_count = 0 + router_backup.next_retry = timezone.now() + datetime.timedelta(minutes=router_backup.router.backup_profile.backup_interval) + router_backup.save() + else: + handle_backup_failure(router_backup, error_message) + return + + +def handle_backup_failure(router_backup: RouterBackup, error_message): + router_backup.error_message = error_message + router_backup.retry_count += 1 + router_backup.next_retry = timezone.now() + datetime.timedelta(minutes=router_backup.router.backup_profile.retry_interval) + router_backup.save() + + +def execute_backup(router_backup: RouterBackup): + error_message = "" + ssh_client = paramiko.SSHClient() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if router_backup.router.router_type == 'routeros': + ssh_client.connect( + router_backup.router.address, username=router_backup.router.username, + password=router_backup.router.password, look_for_keys=False, allow_agent=False, timeout=10 + ) + backup_name = f"backup-routerfleet-{router_backup.schedule_type}-{router_backup.uuid}" + ssh_client.exec_command(f'/system backup save name={backup_name}.backup') + ssh_client.exec_command(f'/export file={backup_name}.rsc') + return True, [f"{backup_name}.backup", f"{backup_name}.rsc"], error_message + else: + error_message = f"Router type not supported: {router_backup.router.get_router_type_display()}" + return False, [], error_message + except Exception as e: + error_message = f"Failed to execute backup: {str(e)}" + return False, [], error_message + finally: + ssh_client.close() + + +def retrieve_backup(router_backup: RouterBackup): + error_message = "" + backup_name = f"backup-routerfleet-{router_backup.schedule_type}-{router_backup.uuid}" + + success = False + ssh_client = paramiko.SSHClient() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + if router_backup.router.router_type == 'routeros': + rsc_file_path = f"/tmp/{backup_name}.rsc" + backup_file_path = f"/tmp/{backup_name}.backup" + + ssh_client.connect(router_backup.router.address, username=router_backup.router.username, + password=router_backup.router.password, look_for_keys=False, allow_agent=False, + timeout=10) + scp_client = SCPClient(ssh_client.get_transport()) + + scp_client.get(f"/{backup_name}.rsc", rsc_file_path) + scp_client.get(f"/{backup_name}.backup", backup_file_path) + + with open(rsc_file_path, 'r') as rsc_file: + rsc_content = rsc_file.read() + rsc_content_cleaned = '\n'.join( + line for line in rsc_content.split('\n') if not line.strip().startswith('#')) + router_backup.backup_text = rsc_content_cleaned + + with open(backup_file_path, 'rb') as backup_file: + router_backup.backup_binary.save(f"{backup_name}.backup", ContentFile(backup_file.read())) + + router_backup.save() + os.remove(rsc_file_path) + os.remove(backup_file_path) + ssh_client.exec_command(f'/file remove "{backup_name}.rsc"') + ssh_client.exec_command(f'/file remove "{backup_name}.backup"') + success = True + else: + error_message = f"Router type not supported: {router_backup.router.get_router_type_display()}" + return success, error_message + + except Exception as e: + return success, f"Failed to retrieve backup files: {str(e)}" + finally: + ssh_client.close() + + return success, error_message + + +def clean_up_backup_files(router_backup: RouterBackup): + ssh_client = paramiko.SSHClient() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if router_backup.router.router_type == 'routeros': + ssh_client.connect( + router_backup.router.address, username=router_backup.router.username, + password=router_backup.router.password, look_for_keys=False, timeout=10, allow_agent=False + ) + ssh_client.exec_command('file remove [find where name~"backup-routerfleet-"]') + else: + print(f"Router type not supported: {router_backup.router.get_router_type_display()}") + except Exception as e: + print(f"Failed to clean up backup files: {str(e)}") + finally: + ssh_client.close() diff --git a/routerlib/functions.py b/routerlib/functions.py new file mode 100644 index 0000000..0d24641 --- /dev/null +++ b/routerlib/functions.py @@ -0,0 +1,69 @@ +import paramiko +import telnetlib + + +def get_router_features(router_type): + if router_type in ['openwrt', 'routeros']: + return ['backup', 'reverse_monitoring', 'ssh', 'ssh_key'] + else: + return [] + + +def test_authentication(router_type, address, username, password, sshkey=None): + router_features = get_router_features(router_type) + if 'ssh' in router_features: + connection_type = 'ssh' + elif 'telnet' in router_features: + connection_type = 'telnet' + else: + return False, 'Router type not supported' + + if connection_type == 'ssh': + return test_ssh_authentication(router_type, address, username, password, sshkey) + elif connection_type == 'telnet': + return test_telnet_authentication(address, username, password, sshkey=None) + + +def test_ssh_authentication(router_type, address, username, password, sshkey=None): + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(address, username=username, password=password, look_for_keys=False, timeout=10, allow_agent=False) + + if router_type == 'routeros': + stdin, stdout, stderr = ssh.exec_command('/system resource print') + output = stdout.read().decode() + if 'platform: MikroTik' in output: + result = True, 'Success: MikroTik device confirmed' + else: + result = False, 'Device is not MikroTik' + elif router_type == 'openwrt': + # Comando para verificar OpenWRT + stdin, stdout, stderr = ssh.exec_command('ubus call system board') + output = stdout.read().decode() + if 'OpenWrt' in output: + result = True, 'Success: OpenWRT device confirmed' + else: + result = False, 'Device is not OpenWRT' + else: + result = False, 'Unsupported device type' + + ssh.close() + return result + except Exception as e: + return False, str(e) + + +def test_telnet_authentication(address, username, password, sshkey=None): + try: + tn = telnetlib.Telnet(address) + tn.read_until(b"login: ") + tn.write(username.encode('ascii') + b"\n") + tn.read_until(b"Password: ") + tn.write(password.encode('ascii') + b"\n") + tn.write(b"exit\n") + tn.close() + return True, 'Success' + except Exception as e: + print(f"Telnet connection failed: {e}") + return False, str(e) diff --git a/routerlib/migrations/__init__.py b/routerlib/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routerlib/models.py b/routerlib/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/routerlib/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/routerlib/tests.py b/routerlib/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/routerlib/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/routerlib/views.py b/routerlib/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/routerlib/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.