mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
snapshot: Fix issues with restore and delete
- 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>
This commit is contained in:
parent
9c4431ec16
commit
20f2ff9370
@ -35,9 +35,6 @@ def parse_arguments():
|
||||
help='Delete a snapshot by number')
|
||||
subparser.add_argument('number', help='Number of snapshot to delete')
|
||||
|
||||
subparser = subparsers.add_parser('delete-all',
|
||||
help='Delete all the snapshots')
|
||||
|
||||
subparser = subparsers.add_parser('set-config',
|
||||
help='Configure automatic snapshots')
|
||||
subparser.add_argument('config')
|
||||
@ -166,25 +163,30 @@ def _add_fstab_entry(mount_point):
|
||||
aug.save()
|
||||
|
||||
|
||||
def _get_snapper_list():
|
||||
command = ['snapper', 'list']
|
||||
process = subprocess.run(command, stdout=subprocess.PIPE, check=True)
|
||||
return process.stdout.decode().splitlines()
|
||||
def _parse_number(number):
|
||||
"""Parse the char following the number and return status of snapshot."""
|
||||
is_default = number[-1] in ('+', '*')
|
||||
is_active = number[-1] in ('-', '*')
|
||||
return number.strip('-+*'), is_default, is_active
|
||||
|
||||
|
||||
def subcommand_list(_):
|
||||
"""List snapshots."""
|
||||
lines = _get_snapper_list()
|
||||
keys = ('number', 'type', 'pre_number', 'date', 'user', 'cleanup',
|
||||
'description')
|
||||
process = subprocess.run(['snapper', 'list'], stdout=subprocess.PIPE,
|
||||
check=True)
|
||||
lines = process.stdout.decode().splitlines()
|
||||
|
||||
keys = ('number', 'is_default', 'is_active', 'type', 'pre_number', 'date',
|
||||
'user', 'cleanup', 'description')
|
||||
snapshots = []
|
||||
for line in lines[2:]:
|
||||
parts = [part.strip('* ') for part in line.split('|')]
|
||||
snapshots.append(dict(zip(keys, parts)))
|
||||
|
||||
default = _get_default_snapshot()
|
||||
for snapshot in snapshots:
|
||||
snapshot['is_default'] = (snapshot['number'] == default)
|
||||
parts = [part.strip() for part in line.split('|')]
|
||||
parts = list(_parse_number(parts[0])) + parts[1:]
|
||||
snapshot = dict(zip(keys, parts))
|
||||
# Snapshot 0 always represents the current system, it need not be
|
||||
# listed and cannot be deleted.
|
||||
if snapshot['number'] != '0':
|
||||
snapshots.append(snapshot)
|
||||
|
||||
snapshots.reverse()
|
||||
print(json.dumps(snapshots))
|
||||
@ -231,35 +233,6 @@ def subcommand_delete(arguments):
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def subcommand_delete_all(_):
|
||||
"""Delete all the snapshots (except the active one)."""
|
||||
lines = _get_snapper_list()
|
||||
snapshot_range = [line.split('|')[0].strip() for line in lines[3:]]
|
||||
default_snapshot = _get_default_snapshot()
|
||||
if snapshot_range:
|
||||
if default_snapshot:
|
||||
index = snapshot_range.index(default_snapshot)
|
||||
range_before = snapshot_range[:index]
|
||||
range_after = snapshot_range[index + 1:]
|
||||
to_delete = [range_before, range_after]
|
||||
else:
|
||||
to_delete = [snapshot_range]
|
||||
|
||||
delete_args = filter(None, map(_get_delete_arg, to_delete))
|
||||
for arg in delete_args:
|
||||
subprocess.run(['snapper', 'delete', arg], check=True)
|
||||
|
||||
|
||||
def _get_delete_arg(range_list):
|
||||
"""Return 'a-b' when given ['a', ..., 'b']."""
|
||||
if not range_list:
|
||||
return None
|
||||
elif len(range_list) == 1:
|
||||
return range_list[0]
|
||||
else:
|
||||
return range_list[0] + '-' + range_list[-1]
|
||||
|
||||
|
||||
def subcommand_set_config(arguments):
|
||||
command = ['snapper', 'set-config'] + arguments.config.split()
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
@ -19,13 +19,11 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snapshot in snapshots %}
|
||||
{% if not snapshot.is_default %}
|
||||
<tr>
|
||||
<td>{{ snapshot.number }}</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>{{ snapshot.description }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ snapshot.number }}</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>{{ snapshot.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -33,6 +31,11 @@
|
||||
<p>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for snapshot in snapshots %}
|
||||
<input type="hidden" name="snapshots" value="{{ snapshot.number }}" />
|
||||
{% endfor %}
|
||||
|
||||
<input type="submit" class="btn btn-danger" name="delete_confirm"
|
||||
value="{% trans 'Delete Snapshots' %}"/>
|
||||
</form>
|
||||
|
||||
@ -19,7 +19,8 @@
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<input type="submit" class="btn btn-danger" name="delete_selected"
|
||||
value="{% trans 'Delete Snapshots' %}"/>
|
||||
value="{% trans 'Delete Snapshots' %}"
|
||||
{{ has_deletable_snapshots|yesno:',disabled="disabled"' }}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,35 +34,38 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snapshot in snapshots %}
|
||||
{% if snapshot.description != "current" %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ snapshot.number }}
|
||||
{% if snapshot.is_default %}
|
||||
<span class="label label-primary">
|
||||
{% trans "active" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>{{ snapshot.description }}</td>
|
||||
<td>
|
||||
<a href="{% url 'snapshot:rollback' snapshot.number %}"
|
||||
class="btn btn-default btn-sm" role="button"
|
||||
title="{% blocktrans trimmed with number=snapshot.number %}
|
||||
Rollback to snapshot #{{ number }}
|
||||
{% endblocktrans %}">
|
||||
<span class="fa fa-repeat"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if not snapshot.is_default %}
|
||||
<input type="checkbox" name="snapshot_list" value={{ snapshot.number }} />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ snapshot.number }}
|
||||
{% if snapshot.is_default %}
|
||||
<span class="label label-default">
|
||||
{% trans "will be used at next boot" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if snapshot.is_active %}
|
||||
<span class="label label-success">
|
||||
{% trans "in use" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>{{ snapshot.description }}</td>
|
||||
<td>
|
||||
<a href="{% url 'snapshot:rollback' snapshot.number %}"
|
||||
class="btn btn-default btn-sm" role="button"
|
||||
title="{% blocktrans trimmed with number=snapshot.number %}
|
||||
Rollback to snapshot #{{ number }}
|
||||
{% endblocktrans %}">
|
||||
<span class="fa fa-repeat"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if not snapshot.is_default and not snapshot.is_active %}
|
||||
<input type="checkbox" name="snapshot_list" value={{ snapshot.number }} />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -4,8 +4,10 @@ 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
|
||||
@ -78,15 +80,21 @@ def manage(request):
|
||||
actions.superuser_run('snapshot', ['create'])
|
||||
messages.success(request, _('Created snapshot.'))
|
||||
if 'delete_selected' in request.POST:
|
||||
if request.POST.getlist('snapshot_list'):
|
||||
snapshot_to_delete = request.POST.getlist('snapshot_list')
|
||||
request.session['snapshots'] = snapshot_to_delete
|
||||
return redirect(reverse('snapshot:delete-selected'))
|
||||
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[1:] if not snapshot['is_default']])
|
||||
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', {
|
||||
@ -100,6 +108,7 @@ def manage(request):
|
||||
|
||||
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]:
|
||||
@ -142,39 +151,44 @@ def update_configuration(request, old_status, new_status):
|
||||
|
||||
|
||||
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':
|
||||
if 'snapshots' in request.session:
|
||||
to_delete = request.session['snapshots']
|
||||
try:
|
||||
if to_delete == len(snapshots):
|
||||
actions.superuser_run('snapshot', ['delete_all'])
|
||||
messages.success(request, _('Deleted all snapshots'))
|
||||
else:
|
||||
for snapshot in to_delete:
|
||||
actions.superuser_run('snapshot', ['delete', snapshot])
|
||||
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
|
||||
try:
|
||||
for snapshot in snapshots_to_delete:
|
||||
actions.superuser_run('snapshot',
|
||||
['delete', snapshot['number']])
|
||||
|
||||
return redirect(reverse('snapshot:manage'))
|
||||
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
|
||||
|
||||
if 'snapshots' in request.session:
|
||||
data = request.session['snapshots']
|
||||
to_delete = list(filter(lambda x: x['number'] in data, snapshots))
|
||||
return redirect(reverse('snapshot:manage'))
|
||||
|
||||
return TemplateResponse(request, 'snapshot_delete_selected.html', {
|
||||
'title': _('Delete Snapshots'),
|
||||
'snapshots': to_delete
|
||||
})
|
||||
return TemplateResponse(request, 'snapshot_delete_selected.html', {
|
||||
'title': _('Delete Snapshots'),
|
||||
'snapshots': snapshots_to_delete
|
||||
})
|
||||
|
||||
|
||||
def rollback(request, number):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user