Manage backup profiles

This commit is contained in:
Eduardo Silva 2024-03-17 17:24:56 -03:00
parent e507987530
commit cc54ba0f73
12 changed files with 392 additions and 18 deletions

131
backup/forms.py Normal file
View file

@ -0,0 +1,131 @@
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, HTML, Field, Div
from .models import BackupProfile
class BackupProfileForm(forms.ModelForm):
class Meta:
model = BackupProfile
fields = [
'name', 'daily_backup', 'weekly_backup', 'monthly_backup',
'daily_retenion', 'weekly_retention', 'monthly_retenion',
'retain_backups_on_error', 'daily_day_monday', 'daily_day_tuesday',
'daily_day_wednesday', 'daily_day_thursday', 'daily_day_friday',
'daily_day_saturday', 'daily_day_sunday', 'weekly_day',
'monthly_day', 'daily_hour', 'weekly_hour', 'monthly_hour',
'max_retry', 'retry_interval', 'backup_interval'
]
# widgets = {
# 'weekly_day': forms.Select(),
# 'monthly_day': forms.Select(),
# 'daily_hour': forms.Select(choices=HOUR_CHOICES),
# 'weekly_hour': forms.Select(choices=HOUR_CHOICES),
# 'monthly_hour': forms.Select(choices=HOUR_CHOICES),
# 'max_retry': forms.Select(),
# 'retry_interval': forms.Select(),
# 'backup_interval': forms.Select(),
# }
def __init__(self, *args, **kwargs):
super(BackupProfileForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
if self.instance.pk:
delete_html = "<a href='javascript:void(0)' class='btn btn-outline-danger' data-command='delete' onclick='openCommandDialog(this)'>Delete</a>"
else:
delete_html = ''
self.fields['daily_day_monday'].label = 'Monday'
self.fields['daily_day_tuesday'].label = 'Tuesday'
self.fields['daily_day_wednesday'].label = 'Wednesday'
self.fields['daily_day_thursday'].label = 'Thursday'
self.fields['daily_day_friday'].label = 'Friday'
self.fields['daily_day_saturday'].label = 'Saturday'
self.fields['daily_day_sunday'].label = 'Sunday'
self.fields['daily_backup'].label = 'Daily'
self.fields['weekly_backup'].label = 'Weekly'
self.fields['monthly_backup'].label = 'Monthly'
self.fields['daily_retenion'].label = 'Retention (days)'
self.fields['weekly_retention'].label = 'Retention (days)'
self.fields['monthly_retenion'].label = 'Retention (days)'
self.helper.layout = Layout(
Div(Div('name', css_class='col-md-12'), css_class='row'),
Div(
Div('daily_backup', css_class='col-md-4'),
Div('weekly_backup', css_class='col-md-4'),
Div('monthly_backup', css_class='col-md-4'),
css_class='row'),
Div(
Div(HTML('<hr><h4>Daily Backups</h4>'), css_class='col-md-12'),
Div('daily_hour', css_class='col-md-4'),
Div('daily_retenion', css_class='col-md-4'),
Div(css_class='col-md-4'),
Div('daily_day_monday', css_class='col-md-4'),
Div('daily_day_tuesday', css_class='col-md-4'),
Div('daily_day_wednesday', css_class='col-md-4'),
Div('daily_day_thursday',css_class='col-md-4'),
Div('daily_day_friday', css_class='col-md-4'),
Div('daily_day_saturday', css_class='col-md-4'),
Div('daily_day_sunday', css_class='col-md-4'),
css_id='daily_settings', css_class='row'
),
Div(
Div(HTML('<hr><h4>Weekly Backups</h4>'), css_class='col-md-12'),
Div('weekly_hour', css_class='col-md-4'),
Div('weekly_retention', css_class='col-md-4'),
Div('weekly_day', css_class='col-md-4'),
css_id='weekly_settings', css_class='row'
),
Div(
Div(HTML('<hr><h4>Monthly Backups</h4>'), css_class='col-md-12'),
Div('monthly_hour', css_class='col-md-4'),
Div('monthly_retenion', css_class='col-md-4'),
Div('monthly_day', css_class='col-md-4'),
css_id='monthly_settings', css_class='row'
),
Div(
Div(HTML('<hr><h4>Backup Settings</h4>'), css_class='col-md-12'),
Div('max_retry', css_class='col-md-4'),
Div('retry_interval', css_class='col-md-4'),
Div('backup_interval', css_class='col-md-4'),
Div('retain_backups_on_error', css_class='col-md-12'),
css_id='misc_settings', css_class='row'
),
Row(
Column(
Submit('submit', 'Salvar', css_class='btn btn-success'),
HTML(' <a class="btn btn-secondary" href="/backup/profile_list/">Back</a> '),
HTML(delete_html),
css_class='col-md-12'
)
)
)
def clean(self):
cleaned_data = super().clean()
daily_backup = cleaned_data.get('daily_backup')
weekly_backup = cleaned_data.get('weekly_backup')
monthly_backup = cleaned_data.get('monthly_backup')
daily_day_monday = cleaned_data.get('daily_day_monday')
daily_day_tuesday = cleaned_data.get('daily_day_tuesday')
daily_day_wednesday = cleaned_data.get('daily_day_wednesday')
daily_day_thursday = cleaned_data.get('daily_day_thursday')
daily_day_friday = cleaned_data.get('daily_day_friday')
daily_day_saturday = cleaned_data.get('daily_day_saturday')
daily_day_sunday = cleaned_data.get('daily_day_sunday')
if daily_backup:
if not daily_day_monday and not daily_day_tuesday and not daily_day_wednesday and not daily_day_thursday and not daily_day_friday and not daily_day_saturday and not daily_day_sunday:
raise forms.ValidationError('You must select at least one day for daily backups')
if not daily_backup and not weekly_backup and not monthly_backup:
raise forms.ValidationError('You must select at least one backup type')
return cleaned_data

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.3 on 2024-03-17 14:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('backup', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='backupprofile',
name='weekly_day',
field=models.CharField(choices=[('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday')], default='sunday', max_length=10),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.3 on 2024-03-17 19:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('backup', '0002_alter_backupprofile_weekly_day'),
]
operations = [
migrations.AlterField(
model_name='backupprofile',
name='retry_interval',
field=models.IntegerField(choices=[(1, '1 Minute'), (15, '15 Minutes'), (30, '30 Minutes'), (60, '1 Hour')], default=30),
),
]

View file

@ -29,7 +29,7 @@ class BackupProfile(models.Model):
daily_day_saturday = models.BooleanField(default=True) daily_day_saturday = models.BooleanField(default=True)
daily_day_sunday = models.BooleanField(default=True) daily_day_sunday = models.BooleanField(default=True)
weekly_day = models.CharField(max_length=10, choices=(('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday'))) weekly_day = models.CharField(max_length=10, default='sunday', choices=(('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday')))
monthly_day = models.IntegerField(default=1, choices=((1, '1st'), (7, '7th'), (14, '14th'), (21, '21st'), (28, '28th'))) monthly_day = models.IntegerField(default=1, choices=((1, '1st'), (7, '7th'), (14, '14th'), (21, '21st'), (28, '28th')))
daily_hour = models.IntegerField(default=3, choices=HOUR_CHOICES) daily_hour = models.IntegerField(default=3, choices=HOUR_CHOICES)
@ -37,7 +37,7 @@ class BackupProfile(models.Model):
monthly_hour = models.IntegerField(default=0, choices=HOUR_CHOICES) monthly_hour = models.IntegerField(default=0, choices=HOUR_CHOICES)
max_retry = models.IntegerField(default=3, choices=((1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'))) max_retry = models.IntegerField(default=3, choices=((1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')))
retry_interval = models.IntegerField(default=30, choices=(('1', '1 Minute'), ('15', '15 Minutes'), ('30', '30 Minutes'), ('60', '1 Hour'))) retry_interval = models.IntegerField(default=30, choices=((1, '1 Minute'), (15, '15 Minutes'), (30, '30 Minutes'), (60, '1 Hour')))
backup_interval = models.IntegerField(default=60, choices=((0, 'No interval'), (5, '5 seconds'), (60, '1 minute'))) backup_interval = models.IntegerField(default=60, choices=((0, 'No interval'), (5, '5 seconds'), (60, '1 minute')))
updated = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now=True)

View file

@ -1,3 +1,48 @@
from django.shortcuts import render from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from .models import BackupProfile
from .forms import BackupProfileForm
from router_manager.models import Router
# Create your views here.
@login_required()
def view_backup_profile_list(request):
context = {
'backup_profile_list': BackupProfile.objects.all().order_by('name'),
'page_title': 'Backup Profiles'
}
return render(request, 'backup/backup_profile_list.html', context)
@login_required()
def view_manage_backup_profile(request):
if request.GET.get('uuid'):
backup_profile = get_object_or_404(BackupProfile, uuid=request.GET.get('uuid'))
if request.GET.get('action') == 'delete':
if request.GET.get('confirmation') == 'delete':
if Router.objects.filter(backup_profile=backup_profile).exists():
messages.warning(request, 'Backup profile in use|Backup profile is in use and cannot be deleted')
return redirect('backup_profile_list')
else:
backup_profile.delete()
messages.success(request, 'Backup profile deleted successfully')
return redirect('backup_profile_list')
else:
messages.warning(request, 'Backup profile not deleted|Invalid confirmation')
return redirect('backup_profile_list')
else:
backup_profile = None
form = BackupProfileForm(request.POST or None, instance=backup_profile)
if form.is_valid():
form.save()
messages.success(request, 'Backup Profile saved successfully')
return redirect('backup_profile_list')
context = {
'form': form,
'page_title': 'Manage Backup Profile',
'instance': backup_profile
}
return render(request, 'backup/backup_profile_form.html', context=context)

View file

@ -11,7 +11,7 @@ class RouterForm(forms.ModelForm):
class Meta: class Meta:
model = Router model = Router
fields = ['name', 'address', 'username', 'password', 'ssh_key', 'monitoring', 'router_type', 'enabled'] fields = ['name', 'address', 'username', 'password', 'ssh_key', 'monitoring', 'router_type', 'enabled', 'backup_profile']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RouterForm, self).__init__(*args, **kwargs) super(RouterForm, self).__init__(*args, **kwargs)
@ -35,6 +35,7 @@ class RouterForm(forms.ModelForm):
css_class='form-row' css_class='form-row'
), ),
'ssh_key', 'ssh_key',
'backup_profile',
'router_type', 'router_type',
'monitoring', 'monitoring',
'enabled', 'enabled',

View file

@ -73,5 +73,3 @@ 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)

View file

@ -4,6 +4,8 @@ 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
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -20,4 +22,7 @@ urlpatterns = [
path('router/ssh_keys/', view_ssh_key_list, name='ssh_keys_list'), path('router/ssh_keys/', view_ssh_key_list, name='ssh_keys_list'),
path('router/manage_group/', view_manage_router_group, name='manage_router_group'), path('router/manage_group/', view_manage_router_group, name='manage_router_group'),
path('router/manage_sshkey/', view_manage_sshkey, name='manage_sshkey'), path('router/manage_sshkey/', view_manage_sshkey, name='manage_sshkey'),
path('backup/profile_list/', view_backup_profile_list, name='backup_profile_list'),
path('backup/manage_profile/', view_manage_backup_profile, name='manage_backup_profile')
] ]

View file

@ -0,0 +1,65 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class='row'>
<div class='{% if form_size %}{{ form_size }}{% else %}col-lg-6{% endif %}'>
<div class="card card-primary card-outline">
{% if page_title %}
<div class="card-header">
<h3 class="card-title">{{ page_title }}</h3>
</div>
{% endif %}
<div class="card-body row">
<div class="col-lg-12">
{% csrf_token %}
{% crispy form %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_page_scripts %}
<script>
function openCommandDialog(element) {
var command = element.getAttribute('data-command');
var confirmation = prompt("{% if delete_confirmation_message %}{{ delete_confirmation_message }}{% else %}Please type 'delete' to proceed.{% endif %}");
if (confirmation) {
var url = "?uuid={{ instance.uuid }}&action=delete&confirmation=" + encodeURIComponent(confirmation);
window.location.href = url;
}
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
function toggleSectionVisibility(checkboxId, sectionId) {
var checkbox = document.getElementById(checkboxId);
var section = document.getElementById(sectionId);
if (checkbox && section) {
section.style.display = checkbox.checked ? '' : 'none';
}
}
var controls = [
{checkboxId: 'id_daily_backup', sectionId: 'daily_settings'},
{checkboxId: 'id_weekly_backup', sectionId: 'weekly_settings'},
{checkboxId: 'id_monthly_backup', sectionId: 'monthly_settings'}
];
controls.forEach(function (control) {
var checkbox = document.getElementById(control.checkboxId);
if (checkbox) {
checkbox.addEventListener('change', function () {
toggleSectionVisibility(control.checkboxId, control.sectionId);
});
toggleSectionVisibility(control.checkboxId, control.sectionId);
}
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% block content %}
<div class='row'>
<div class='col-lg-12'>
<div class="card card-primary card-outline">
{% if page_title %}
<div class="card-header">
<h3 class="card-title">{{ page_title }}</h3>
</div>
{% endif %}
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Router Count</th>
<th>Daily</th>
<th>Weekly</th>
<th>Monthly</th>
<th></th>
</tr>
</thead>
<tbody>
{% for backup_profile in backup_profile_list %}
<tr>
<td>{{ backup_profile.name }}</td>
<td>
{{ backup_profile.router_set.count }}
</td>
<td>
{% if backup_profile.daily_backup %}
<i class="far fa-check-circle text-success"></i>
{% endif %}
</td>
<td>
{% if backup_profile.weekly_backup %}
<i class="far fa-check-circle text-success"></i>
{% endif %}
</td>
<td>
{% if backup_profile.monthly_backup %}
<i class="far fa-check-circle text-success"></i>
{% endif %}
</td>
<td class="min-width">
<a href="/backup/manage_profile/?uuid={{ backup_profile.uuid }}"><i class="fas fa-edit"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<a href="/backup/manage_profile/" class="btn btn-primary">Add Backup Profile</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -111,7 +111,17 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-item">
<a href="/backup/profile_list/" class="nav-link {% if '/backup/' in request.path %}active{% endif %}">
<i class="fas fa-box-open"></i>
<p>
Backup
</p>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a href="/user/list/" class="nav-link {% if '/user/' in request.path %}active{% endif %}"> <a href="/user/list/" class="nav-link {% if '/user/' in request.path %}active{% endif %}">
<i class="fas fa-users nav-icon"></i> <i class="fas fa-users nav-icon"></i>

View file

@ -19,40 +19,48 @@
<th>Name</th> <th>Name</th>
<th>Type</th> <th>Type</th>
<th>Address</th> <th>Address</th>
<th>Enabled</th>
<th>Status</th> <th>Status</th>
<th>Backup</th>
<th>Groups</th> <th>Groups</th>
<th>Authentication</th> <th>Auth</th>
<th></th> <th></th>
</thead> </thead>
<tbody> <tbody>
{% for router in router_list %} {% for router in router_list %}
<tr> <tr {% if not router.enabled %}style="text-decoration: line-through;"{% endif %}>
<td>{{ router.name }}</td> <td>{{ router.name }}</td>
<td>{{ router.get_router_type_display }}</td> <td>{{ router.get_router_type_display }}</td>
<td>{{ router.address }}</td> <td>{{ router.address }}</td>
<td>{{ router.enabled }}</td>
<td> <td>
{% if router.monitoring %} {% if router.monitoring %}
{% if router.routerstatus.status_online %} {% if router.routerstatus.status_online %}
Online <i class="far fa-check-circle text-success" title="Host is Online"></i>
{% else %} {% else %}
Offline <i class="far fa-times-circle text-danger" title="Host is unavailable"></i>
{% endif %} {% endif %}
{% else %} {% else %}
--- ---
{% endif %} {% endif %}
</td> </td>
<td>
{% if router.backup_profile %}
{{ router.backup_profile }} {% if router.routerstatus.last_backup_failed %}<i class="fas fa-exclamation-triangle text-danger" title="Last backup failed to complete"></i>{% endif %}
{% else %}
<i class="fas fa-exclamation-triangle text-warning" title="No backup profile selected"></i>
{% endif %}
</td>
<td> <td>
{{ router.routergroup_set.count }} {{ router.routergroup_set.count }}
</td> </td>
<td> <td class="min-width">
{% if router.ssh_key %} {% if router.ssh_key %}
{{ router.ssh_key }} <i class="fas fa-key" title="SSH Key: {{ router.ssh_key }}"></i>
{% elif router.password %} {% elif router.password %}
User/Password <i class="fas fa-keyboard" title="Password Authentication"></i>
{% else %} {% else %}
Missing authentication <i class="fas fa-exclamation-triangle text-warning" title="Missing authentication"></i>
{% endif %} {% endif %}
</td> </td>
<td class="min-width"> <td class="min-width">