diff --git a/actions/snapshot b/actions/snapshot index e21197baf..778f54de0 100755 --- a/actions/snapshot +++ b/actions/snapshot @@ -15,17 +15,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ Configuration helper for filesystem snapshots. """ -import augeas import argparse import json import os import subprocess +import augeas + FSTAB = '/etc/fstab' AUG_FSTAB = '/files/etc/fstab' @@ -39,9 +39,13 @@ def parse_arguments(): subparsers.add_parser('list', help='List snapshots') subparsers.add_parser('create', help='Create snapshot') - subparser = subparsers.add_parser('delete', help='Delete snapshot') + subparser = subparsers.add_parser('delete', + 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('rollback', help='Rollback to snapshot') subparser.add_argument('number', help='Number of snapshot to rollback to') @@ -68,8 +72,8 @@ def _add_fstab_entry(mount_point): """Add mountpoint for subvolumes.""" snapshots_mount_point = os.path.join(mount_point, '.snapshots') - aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + - augeas.Augeas.NO_MODL_AUTOLOAD) + aug = augeas.Augeas( + flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) aug.set('/augeas/load/Fstab/lens', 'Fstab.lns') aug.set('/augeas/load/Fstab/incl[last() + 1]', FSTAB) aug.load() @@ -95,11 +99,15 @@ def _add_fstab_entry(mount_point): aug.save() -def subcommand_list(_): - """List snapshots.""" +def _get_snapper_list(): command = ['snapper', 'list'] process = subprocess.run(command, stdout=subprocess.PIPE, check=True) - lines = process.stdout.decode().splitlines() + return process.stdout.decode().splitlines() + + +def subcommand_list(_): + """List snapshots.""" + lines = _get_snapper_list() keys = ('type', 'number', 'pre_number', 'date', 'user', 'cleanup', 'description') snapshots = [] @@ -137,15 +145,30 @@ def subcommand_create(_): def subcommand_delete(arguments): - """Delete snapshot.""" + """Delete a snapshot by number.""" command = ['snapper', 'delete', arguments.number] subprocess.run(command, check=True) +def subcommand_delete_all(_): + """Delete all the snapshots.""" + lines = _get_snapper_list() + snapshot_range = [line.split('|')[1].strip() for line in lines[3:]] + if snapshot_range: + if len(snapshot_range) == 1: + to_delete = snapshot_range[0] + else: + to_delete = '-'.join([snapshot_range[0], snapshot_range[-1]]) + command = ['snapper', 'delete', to_delete] + subprocess.run(command, check=True) + + def subcommand_rollback(arguments): """Rollback to snapshot.""" - command = ['snapper', 'rollback', '--description', 'created by rollback', - arguments.number] + command = [ + 'snapper', 'rollback', '--description', 'created by rollback', + arguments.number + ] subprocess.run(command, check=True) diff --git a/plinth/modules/snapshot/templates/snapshot.html b/plinth/modules/snapshot/templates/snapshot.html index 3fc81e2d5..72c60005c 100644 --- a/plinth/modules/snapshot/templates/snapshot.html +++ b/plinth/modules/snapshot/templates/snapshot.html @@ -22,6 +22,29 @@ {% load i18n %} {% block configuration %} +

+

+
+ {% csrf_token %} +
+ +
+
+ + +
+

-

-

- {% csrf_token %} - - -
-

- {% endblock %} diff --git a/plinth/modules/snapshot/templates/snapshot_delete.html b/plinth/modules/snapshot/templates/snapshot_delete.html index 92d285c7c..f94eb791c 100644 --- a/plinth/modules/snapshot/templates/snapshot_delete.html +++ b/plinth/modules/snapshot/templates/snapshot_delete.html @@ -49,7 +49,7 @@
{% csrf_token %} - diff --git a/plinth/modules/snapshot/templates/snapshot_delete_all.html b/plinth/modules/snapshot/templates/snapshot_delete_all.html new file mode 100644 index 000000000..c83fd243a --- /dev/null +++ b/plinth/modules/snapshot/templates/snapshot_delete_all.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} +

{{ title }}

+ +

{% trans "Delete the following snapshots permanently?" %}

+ +
+
+ + + + + + + + {% for snapshot in snapshots %} + + + + + + {% endfor %} + +
{% trans "Number" %}{% trans "Date" %}{% trans "Description" %}
{{ snapshot.number }}{{ snapshot.date }}{{ snapshot.description }}
+
+
+ +

+ + {% csrf_token %} + + +

+

+ +{% endblock %} diff --git a/plinth/modules/snapshot/urls.py b/plinth/modules/snapshot/urls.py index 2dc1403a7..3ffd9d651 100644 --- a/plinth/modules/snapshot/urls.py +++ b/plinth/modules/snapshot/urls.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ URLs for the snapshot module. """ @@ -23,10 +22,10 @@ from django.conf.urls import url from . import views - urlpatterns = [ url(r'^sys/snapshot/$', views.index, name='index'), url(r'^sys/snapshot/(?P\d+)/delete$', views.delete, name='delete'), + url(r'^sys/snapshot/all/delete$', views.delete_all, name='delete-all'), url(r'^sys/snapshot/(?P\d+)/rollback$', views.rollback, name='rollback'), ] diff --git a/plinth/modules/snapshot/views.py b/plinth/modules/snapshot/views.py index a300760ac..0a714be4c 100644 --- a/plinth/modules/snapshot/views.py +++ b/plinth/modules/snapshot/views.py @@ -14,17 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ Views for snapshot module. """ +import json + from django.contrib import messages from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse from django.utils.translation import ugettext as _ -import json from plinth import actions from plinth.modules import snapshot as snapshot_module @@ -39,40 +39,55 @@ def index(request): output = actions.superuser_run('snapshot', ['list']) snapshots = json.loads(output) - return TemplateResponse(request, 'snapshot.html', - {'title': snapshot_module.name, - 'description': snapshot_module.description, - 'snapshots': snapshots}) + return TemplateResponse(request, 'snapshot.html', { + 'title': snapshot_module.name, + 'description': snapshot_module.description, + 'snapshots': snapshots + }) def delete(request, number): """Show confirmation to delete a snapshot.""" if request.method == 'POST': actions.superuser_run('snapshot', ['delete', number]) - messages.success( - request, _('Deleted snapshot #{number}.').format(number=number)) + messages.success(request, + _('Deleted snapshot #{number}.').format( + number=number)) + return redirect(reverse('snapshot:index')) + + output = actions.superuser_run('snapshot', ['list']) + snapshots = json.loads(output) + snapshot = next(s for s in snapshots if s['number'] == number) + + return TemplateResponse(request, 'snapshot_delete.html', { + 'title': _('Delete Snapshot'), + 'snapshot': snapshot + }) + + +def delete_all(request): + """Show confirmation to delete all snapshots.""" + if request.method == 'POST': + actions.superuser_run('snapshot', ['delete-all']) + messages.success(request, _('Deleted all snapshots.')) return redirect(reverse('snapshot:index')) 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_delete.html', - {'title': _('Delete Snapshot'), - 'snapshot': snapshot}) + return TemplateResponse(request, 'snapshot_delete_all.html', { + 'title': _('Delete Snapshots'), + 'snapshots': snapshots[1:] + }) 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.success(request, + _('Rolled back to snapshot #{number}.').format( + number=number)) messages.warning( request, _('The system must be restarted to complete the rollback.')) @@ -86,6 +101,7 @@ def rollback(request, number): if current_snapshot['number'] == number: snapshot = current_snapshot - return TemplateResponse(request, 'snapshot_rollback.html', - {'title': _('Rollback to Snapshot'), - 'snapshot': snapshot}) + return TemplateResponse(request, 'snapshot_rollback.html', { + 'title': _('Rollback to Snapshot'), + 'snapshot': snapshot + })