mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-02-04 08:13:38 +00:00
- Avoid no-response error when deleting a snapshot. This is caused when disk is full and delete operation tries to store data in session which is stored on disk. The session update fails and there are no values to delete. This case in not handled and return a None in view causing a 500 error. Use GET params instead. - Delete all functionality that is meant to speed up deleting snapshots has regressed and is currently never used. Further, there are more types of snapshots that can't be deleted that needs to be handled in delete all functionality. Drop it for now. - When snapper list is run the snapshot number can contain '-', '+' or '*' suffixed to it. Currently only '*' is handled. This leads to failure in listing the snapshots after a restore snapshot'. Fix this is properly parsing. Also it is no longer needed to query 'btrfs' command to know the snapshot that will used at next boot. '+' or '*' means that. - Don't list snapshot number '0'. It is never listed to the user and it can never be deleted. It represents the current system. - Properly implement checking for default and active snapshots. Don't let delete operation on either of them. - Fix regression with disabling the delete button when there are no snapshots that can be deleted. Tests performed: - Before any snapshot is restored, the labels 'will be used at next boot' and 'in use' are not shown. Snapshot with number 0 is not shown. - Immediately after restoring a snapshot, the 'will be used at next boot' label will shown up on snapshot that is going to boot next. - After rebooting after restore, the snapshot that has been restored will show 'will be used at next boot' and 'in use' labels. Restoring another snapshot will move the 'will be used at next boot' label to the new restore snapshot but keep the 'in use' label on the current snapshot until next reboot. Snapshot with number 0 is not shown. - Delete check boxes are not shown against the 'in use' and 'will be used at next boot' snapshots. Entering their values manually in the URL in the delete screen will lead them to be ignored. - Select multiple snapshots and click delete. The details appear properly in the confirmation window. Deleting will delete the snapshots. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
218 lines
7.3 KiB
Python
218 lines
7.3 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Views for snapshot module.
|
|
"""
|
|
|
|
import json
|
|
import urllib.parse
|
|
|
|
from django.contrib import messages
|
|
from django.http import HttpResponseRedirect
|
|
from django.shortcuts import redirect
|
|
from django.template.response import TemplateResponse
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils.translation import ugettext as _
|
|
from django.utils.translation import ugettext_lazy
|
|
|
|
from plinth import actions
|
|
from plinth.errors import ActionError
|
|
from plinth.modules import snapshot as snapshot_module
|
|
from plinth.modules import storage
|
|
|
|
from . import get_configuration
|
|
from .forms import SnapshotForm
|
|
|
|
subsubmenu = [
|
|
{
|
|
'url': reverse_lazy('snapshot:index'),
|
|
'text': ugettext_lazy('Configure')
|
|
},
|
|
{
|
|
'url': reverse_lazy('snapshot:manage'),
|
|
'text': ugettext_lazy('Manage Snapshots')
|
|
},
|
|
]
|
|
|
|
|
|
def not_supported_view(request):
|
|
"""Show that snapshots are not supported on the system."""
|
|
template_data = {
|
|
'app_info': snapshot_module.app.info,
|
|
'title': snapshot_module.app.info.name,
|
|
'fs_type': storage.get_filesystem_type(),
|
|
'fs_types_supported': snapshot_module.fs_types_supported,
|
|
}
|
|
return TemplateResponse(request, 'snapshot_not_supported.html',
|
|
template_data)
|
|
|
|
|
|
def index(request):
|
|
"""Show snapshot list."""
|
|
if not snapshot_module.is_supported():
|
|
return not_supported_view(request)
|
|
|
|
status = get_configuration()
|
|
if request.method == 'POST':
|
|
form = SnapshotForm(request.POST)
|
|
if 'update' in request.POST and form.is_valid():
|
|
update_configuration(request, status, form.cleaned_data)
|
|
status = get_configuration()
|
|
form = SnapshotForm(initial=status)
|
|
else:
|
|
form = SnapshotForm(initial=status)
|
|
|
|
return TemplateResponse(
|
|
request, 'snapshot.html', {
|
|
'app_info': snapshot_module.app.info,
|
|
'title': snapshot_module.app.info.name,
|
|
'subsubmenu': subsubmenu,
|
|
'form': form
|
|
})
|
|
|
|
|
|
def manage(request):
|
|
"""Show snapshot list."""
|
|
if not snapshot_module.is_supported():
|
|
return not_supported_view(request)
|
|
|
|
if request.method == 'POST':
|
|
if 'create' in request.POST:
|
|
actions.superuser_run('snapshot', ['create'])
|
|
messages.success(request, _('Created snapshot.'))
|
|
if 'delete_selected' in request.POST:
|
|
to_delete = request.POST.getlist('snapshot_list')
|
|
if to_delete:
|
|
# Send values using GET params instead of session variables so
|
|
# that snapshots can be deleted even when disk is full.
|
|
params = [('snapshots', number) for number in to_delete]
|
|
params = urllib.parse.urlencode(params)
|
|
url = reverse('snapshot:delete-selected')
|
|
return HttpResponseRedirect(f'{url}?{params}')
|
|
|
|
output = actions.superuser_run('snapshot', ['list'])
|
|
snapshots = json.loads(output)
|
|
has_deletable_snapshots = any([
|
|
snapshot for snapshot in snapshots
|
|
if not snapshot['is_default'] and not snapshot['is_active']
|
|
])
|
|
|
|
return TemplateResponse(
|
|
request, 'snapshot_manage.html', {
|
|
'title': snapshot_module.app.info.name,
|
|
'app_info': snapshot_module.app.info,
|
|
'snapshots': snapshots,
|
|
'has_deletable_snapshots': has_deletable_snapshots,
|
|
'subsubmenu': subsubmenu,
|
|
})
|
|
|
|
|
|
def update_configuration(request, old_status, new_status):
|
|
"""Update configuration of snapshots."""
|
|
|
|
def make_config(args):
|
|
key, stamp = args[0], args[1]
|
|
if old_status[key] != new_status[key]:
|
|
if 'limit' in key:
|
|
return stamp.format('0-{}'.format(new_status[key]))
|
|
|
|
return stamp.format(new_status[key])
|
|
|
|
return None
|
|
|
|
config = filter(
|
|
None,
|
|
map(make_config, [
|
|
('enable_timeline_snapshots', 'TIMELINE_CREATE={}'),
|
|
('hourly_limit', 'TIMELINE_LIMIT_HOURLY={}'),
|
|
('daily_limit', 'TIMELINE_LIMIT_DAILY={}'),
|
|
('weekly_limit', 'TIMELINE_LIMIT_WEEKLY={}'),
|
|
('monthly_limit', 'TIMELINE_LIMIT_MONTHLY={}'),
|
|
('yearly_limit', 'TIMELINE_LIMIT_YEARLY={}'),
|
|
('free_space', 'FREE_LIMIT={}'),
|
|
]))
|
|
|
|
if old_status['enable_software_snapshots'] != new_status[
|
|
'enable_software_snapshots']:
|
|
if new_status['enable_software_snapshots'] == 'yes':
|
|
actions.superuser_run('snapshot', ['disable-apt-snapshot', 'no'])
|
|
else:
|
|
actions.superuser_run('snapshot', ['disable-apt-snapshot', 'yes'])
|
|
|
|
try:
|
|
actions.superuser_run('snapshot', ['set-config', " ".join(config)])
|
|
|
|
messages.success(request, _('Storage snapshots configuration updated'))
|
|
except ActionError as exception:
|
|
messages.error(
|
|
request,
|
|
_('Action error: {0} [{1}] [{2}]').format(exception.args[0],
|
|
exception.args[1],
|
|
exception.args[2]))
|
|
|
|
|
|
def delete_selected(request):
|
|
"""View to delete selected snapshots."""
|
|
if request.method == 'POST':
|
|
to_delete = set(request.POST.getlist('snapshots'))
|
|
else:
|
|
to_delete = set(request.GET.getlist('snapshots'))
|
|
|
|
if not to_delete:
|
|
return redirect(reverse('snapshot:manage'))
|
|
|
|
output = actions.superuser_run('snapshot', ['list'])
|
|
snapshots = json.loads(output)
|
|
snapshots_to_delete = [
|
|
snapshot for snapshot in snapshots if snapshot['number'] in to_delete
|
|
and not snapshot['is_active'] and not snapshot['is_default']
|
|
]
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
for snapshot in snapshots_to_delete:
|
|
actions.superuser_run('snapshot',
|
|
['delete', snapshot['number']])
|
|
|
|
messages.success(request, _('Deleted selected snapshots'))
|
|
except ActionError as exception:
|
|
if 'Config is in use.' in exception.args[2]:
|
|
messages.error(
|
|
request,
|
|
_('Snapshot is currently in use. '
|
|
'Please try again later.'))
|
|
else:
|
|
raise
|
|
|
|
return redirect(reverse('snapshot:manage'))
|
|
|
|
return TemplateResponse(request, 'snapshot_delete_selected.html', {
|
|
'title': _('Delete Snapshots'),
|
|
'snapshots': snapshots_to_delete
|
|
})
|
|
|
|
|
|
def rollback(request, number):
|
|
"""Show confirmation to rollback to a snapshot."""
|
|
if request.method == 'POST':
|
|
actions.superuser_run('snapshot', ['rollback', number])
|
|
messages.success(
|
|
request,
|
|
_('Rolled back to snapshot #{number}.').format(number=number))
|
|
messages.warning(
|
|
request,
|
|
_('The system must be restarted to complete the rollback.'))
|
|
return redirect(reverse('power:restart'))
|
|
|
|
output = actions.superuser_run('snapshot', ['list'])
|
|
snapshots = json.loads(output)
|
|
|
|
snapshot = None
|
|
for current_snapshot in snapshots:
|
|
if current_snapshot['number'] == number:
|
|
snapshot = current_snapshot
|
|
|
|
return TemplateResponse(request, 'snapshot_rollback.html', {
|
|
'title': _('Rollback to Snapshot'),
|
|
'snapshot': snapshot
|
|
})
|