mirror of
https://github.com/eduardogsilva/routerfleet.git
synced 2025-07-31 16:24:29 +02:00
Mikrotik backup
This commit is contained in:
parent
c9a7a69d4b
commit
7d2cd9a7dd
16 changed files with 298 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,3 +7,4 @@
|
||||||
routerfleet/production_settings.py
|
routerfleet/production_settings.py
|
||||||
.idea/
|
.idea/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
backups/
|
|
@ -1,6 +1,9 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
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.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
|
from routerlib.backup_functions import perform_backup
|
||||||
from .models import BackupProfile
|
from .models import BackupProfile
|
||||||
from .forms import BackupProfileForm
|
from .forms import BackupProfileForm
|
||||||
from router_manager.models import Router
|
from router_manager.models import Router
|
||||||
|
@ -78,3 +81,14 @@ def view_backup_details(request):
|
||||||
'page_title': 'Backup Details'
|
'page_title': 'Backup Details'
|
||||||
}
|
}
|
||||||
return render(request, 'backup/backup_details.html', context)
|
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)
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,6 +7,7 @@ class RouterBackup(models.Model):
|
||||||
router = models.ForeignKey(Router, on_delete=models.CASCADE)
|
router = models.ForeignKey(Router, on_delete=models.CASCADE)
|
||||||
success = models.BooleanField(default=False)
|
success = models.BooleanField(default=False)
|
||||||
error = models.BooleanField(default=False)
|
error = models.BooleanField(default=False)
|
||||||
|
backup_pending_retrieval = models.BooleanField(default=False)
|
||||||
error_message = models.TextField(blank=True, null=True)
|
error_message = models.TextField(blank=True, null=True)
|
||||||
retry_count = models.IntegerField(default=0)
|
retry_count = models.IntegerField(default=0)
|
||||||
next_retry = models.DateTimeField(blank=True, null=True)
|
next_retry = models.DateTimeField(blank=True, null=True)
|
||||||
|
@ -15,6 +16,7 @@ class RouterBackup(models.Model):
|
||||||
queue_length = models.IntegerField(default=0) # Seconds
|
queue_length = models.IntegerField(default=0) # Seconds
|
||||||
finish_time = models.DateTimeField(blank=True, null=True)
|
finish_time = models.DateTimeField(blank=True, null=True)
|
||||||
backup_text = models.TextField(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)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
asgiref==3.7.2
|
asgiref==3.7.2
|
||||||
|
bcrypt==4.1.2
|
||||||
|
cffi==1.16.0
|
||||||
crispy-bootstrap4==2024.1
|
crispy-bootstrap4==2024.1
|
||||||
crispy-bootstrap5==2024.2
|
crispy-bootstrap5==2024.2
|
||||||
|
cryptography==42.0.5
|
||||||
Django==5.0.3
|
Django==5.0.3
|
||||||
django-crispy-forms==2.1
|
django-crispy-forms==2.1
|
||||||
|
paramiko==3.4.0
|
||||||
|
pycparser==2.21
|
||||||
|
PyNaCl==1.5.0
|
||||||
|
scp==0.14.5
|
||||||
sqlparse==0.4.4
|
sqlparse==0.4.4
|
||||||
typing_extensions==4.10.0
|
typing_extensions==4.10.0
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django import forms
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Submit, Row, Column, HTML
|
from crispy_forms.layout import Layout, Submit, Row, Column, HTML
|
||||||
from .models import Router, RouterGroup, SSHKey
|
from .models import Router, RouterGroup, SSHKey
|
||||||
|
from routerlib.functions import test_authentication
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
@ -82,7 +83,15 @@ class RouterForm(forms.ModelForm):
|
||||||
ipaddress.ip_address(address)
|
ipaddress.ip_address(address)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise forms.ValidationError('The address field must be a valid hostname or IP address.')
|
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
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ from dashboard.views import view_dashboard, view_status
|
||||||
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_router_list, view_manage_router, view_router_group_list, view_ssh_key_list, view_manage_router_group, view_manage_sshkey
|
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 = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('debug/run_backups/', view_debug_run_backups, name='debug_run_backups'),
|
||||||
path('', view_dashboard, name='dashboard'),
|
path('', view_dashboard, name='dashboard'),
|
||||||
path('status/', view_status, name='status'),
|
path('status/', view_status, name='status'),
|
||||||
path('user/list/', view_user_list, name='user_list'),
|
path('user/list/', view_user_list, name='user_list'),
|
||||||
|
|
0
routerlib/__init__.py
Normal file
0
routerlib/__init__.py
Normal file
3
routerlib/admin.py
Normal file
3
routerlib/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
routerlib/apps.py
Normal file
6
routerlib/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RouterlibConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'routerlib'
|
150
routerlib/backup_functions.py
Normal file
150
routerlib/backup_functions.py
Normal file
|
@ -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()
|
69
routerlib/functions.py
Normal file
69
routerlib/functions.py
Normal file
|
@ -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)
|
0
routerlib/migrations/__init__.py
Normal file
0
routerlib/migrations/__init__.py
Normal file
3
routerlib/models.py
Normal file
3
routerlib/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
3
routerlib/tests.py
Normal file
3
routerlib/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
routerlib/views.py
Normal file
3
routerlib/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
Loading…
Add table
Add a link
Reference in a new issue