mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-07-22 11:59:33 +00:00
snapshot: New module for disk snapshots
- Create and list filesystem snapshots. Hide "current" snapshot. - Allow deleting snapshots, except for default subvolume. - Allow rollback to a snapshot.
This commit is contained in:
parent
62934e7edc
commit
5e18a648e1
130
actions/snapshot
Executable file
130
actions/snapshot
Executable file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Configuration helper for filesystem snapshots.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
FSTAB = '/etc/fstab'
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Return parsed command line arguments as dictionary."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||
|
||||
subparsers.add_parser('setup', help='Configure snapper')
|
||||
subparsers.add_parser('list', help='List snapshots')
|
||||
subparsers.add_parser('create', help='Create snapshot')
|
||||
|
||||
subparser = subparsers.add_parser('delete', help='Delete snapshot')
|
||||
subparser.add_argument('number', help='Number of snapshot to delete')
|
||||
|
||||
subparser = subparsers.add_parser('rollback', help='Rollback to snapshot')
|
||||
subparser.add_argument('number', help='Number of snapshot to rollback to')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def subcommand_setup(_):
|
||||
"""Configure snapper."""
|
||||
# Check if root config exists.
|
||||
command = ['snapper', 'list-configs']
|
||||
process = subprocess.run(command, stdout=subprocess.PIPE, check=True)
|
||||
output = process.stdout.decode()
|
||||
|
||||
# Create root config if needed.
|
||||
if 'root' not in output:
|
||||
command = ['snapper', 'create-config', '/']
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
# Add mountpoint for subvolumes.
|
||||
with open(FSTAB, 'r') as fstab:
|
||||
lines = fstab.readlines()
|
||||
|
||||
spec = None
|
||||
for line in lines:
|
||||
if '.snapshots' in line:
|
||||
return
|
||||
if 'btrfs' in line:
|
||||
spec = line.split(' ')[0]
|
||||
|
||||
if spec:
|
||||
with open(FSTAB, 'a') as fstab:
|
||||
fstab.write(spec + ' /.snapshots btrfs subvol=.snapshots 0 1\n')
|
||||
|
||||
|
||||
def subcommand_list(_):
|
||||
"""List snapshots."""
|
||||
command = ['snapper', 'list']
|
||||
process = subprocess.run(command, stdout=subprocess.PIPE, check=True)
|
||||
lines = process.stdout.decode().splitlines()
|
||||
keys = ('type', 'number', '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)))
|
||||
|
||||
# Mark default subvolume.
|
||||
command = ['btrfs', 'subvolume', 'get-default', '/']
|
||||
process = subprocess.run(command, stdout=subprocess.PIPE, check=True)
|
||||
output = process.stdout.decode()
|
||||
default = None
|
||||
if '.snapshots' in output:
|
||||
default = output.split('/')[1]
|
||||
for snapshot in snapshots:
|
||||
snapshot['is_default'] = snapshot['number'] == default
|
||||
|
||||
print(json.dumps(snapshots))
|
||||
|
||||
|
||||
def subcommand_create(_):
|
||||
"""Create snapshot."""
|
||||
command = ['snapper', 'create', '--description', 'manually created']
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def subcommand_delete(arguments):
|
||||
"""Delete snapshot."""
|
||||
command = ['snapper', 'delete', arguments.number]
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def subcommand_rollback(arguments):
|
||||
"""Rollback to snapshot."""
|
||||
command = ['snapper', 'rollback', '--description', 'created by rollback',
|
||||
arguments.number]
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse arguments and perform all duties."""
|
||||
arguments = parse_arguments()
|
||||
|
||||
subcommand = arguments.subcommand.replace('-', '_')
|
||||
subcommand_method = globals()['subcommand_' + subcommand]
|
||||
subcommand_method(arguments)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
data/etc/plinth/modules-enabled/snapshot
Normal file
1
data/etc/plinth/modules-enabled/snapshot
Normal file
@ -0,0 +1 @@
|
||||
plinth.modules.snapshot
|
||||
53
plinth/modules/snapshot/__init__.py
Normal file
53
plinth/modules/snapshot/__init__.py
Normal file
@ -0,0 +1,53 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Plinth module to manage filesystem snapshots.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth import actions
|
||||
from plinth import cfg
|
||||
|
||||
|
||||
version = 1
|
||||
|
||||
depends = ['system']
|
||||
|
||||
managed_packages = ['snapper']
|
||||
|
||||
title = _('Snapshots')
|
||||
|
||||
description = [
|
||||
_('Snapshots allows creating and managing filesystem snapshots. These can '
|
||||
'be used to roll back the system to a previous state.')
|
||||
]
|
||||
|
||||
service = None
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the module."""
|
||||
menu = cfg.main_menu.get('system:index')
|
||||
menu.add_urlname(title, 'glyphicon-film', 'snapshot:index')
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
helper.install(managed_packages)
|
||||
helper.call('post', actions.superuser_run, 'snapshot', ['setup'])
|
||||
85
plinth/modules/snapshot/templates/snapshot.html
Normal file
85
plinth/modules/snapshot/templates/snapshot.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ title }}</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<th>{% trans "Number" %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
<th>{% trans "Rollback" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snapshot in snapshots %}
|
||||
{% if snapshot.description != "current" %}
|
||||
<tr>
|
||||
<td>{{ snapshot.number }}</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>
|
||||
{% if snapshot.is_default %}
|
||||
{% trans "[default subvolume]" %}<br />
|
||||
{% endif %}
|
||||
{{ snapshot.description }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'snapshot:rollback' snapshot.number %}"
|
||||
class="btn btn-default btn-sm"
|
||||
role="button"
|
||||
title="{% blocktrans with number=snapshot.number %}Rollback to snapshot #{{ number }}{% endblocktrans %}">
|
||||
<span class="glyphicon glyphicon-repeat"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if not snapshot.is_default %}
|
||||
<a href="{% url 'snapshot:delete' snapshot.number %}"
|
||||
class="btn btn-default btn-sm"
|
||||
role="button"
|
||||
title="{% blocktrans with number=snapshot.number %}Delete snapshot #{{ number }}{% endblocktrans %}">
|
||||
<span class="glyphicon glyphicon-trash"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% trans "Create Snapshot" %}"/>
|
||||
</form>
|
||||
</p>
|
||||
{% endblock %}
|
||||
61
plinth/modules/snapshot/templates/snapshot_delete.html
Normal file
61
plinth/modules/snapshot/templates/snapshot_delete.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ title }}</h2>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The snapshot will be permanently deleted. Please confirm.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<th>{% trans "Number" %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ snapshot.number }}</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>{{ snapshot.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% blocktrans with number=snapshot.number %}Delete Snapshot #{{ number }}{% endblocktrans %}"/>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
61
plinth/modules/snapshot/templates/snapshot_rollback.html
Normal file
61
plinth/modules/snapshot/templates/snapshot_rollback.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ title }}</h2>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The system will be rolled back to the selected snapshot. Please confirm.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<th>{% trans "Number" %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ snapshot.number }}</td>
|
||||
<td>{{ snapshot.date }}</td>
|
||||
<td>{{ snapshot.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% blocktrans with number=snapshot.number %}Rollback to Snapshot #{{ number }}{% endblocktrans %}"/>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
0
plinth/modules/snapshot/tests/__init__.py
Normal file
0
plinth/modules/snapshot/tests/__init__.py
Normal file
32
plinth/modules/snapshot/urls.py
Normal file
32
plinth/modules/snapshot/urls.py
Normal file
@ -0,0 +1,32 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
URLs for the snapshot module.
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sys/snapshot/$', views.index, name='index'),
|
||||
url(r'^sys/snapshot/(?P<number>\d+)/delete$', views.delete, name='delete'),
|
||||
url(r'^sys/snapshot/(?P<number>\d+)/rollback$', views.rollback,
|
||||
name='rollback'),
|
||||
]
|
||||
87
plinth/modules/snapshot/views.py
Normal file
87
plinth/modules/snapshot/views.py
Normal file
@ -0,0 +1,87 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Views for snapshot module.
|
||||
"""
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
import json
|
||||
|
||||
from plinth import actions
|
||||
from plinth.modules import snapshot as snapshot_module
|
||||
|
||||
|
||||
def index(request):
|
||||
"""Show snapshot list."""
|
||||
if request.method == 'POST':
|
||||
actions.superuser_run('snapshot', ['create'])
|
||||
messages.success(request, _('Created snapshot.'))
|
||||
|
||||
output = actions.superuser_run('snapshot', ['list'])
|
||||
snapshots = json.loads(output)
|
||||
|
||||
return TemplateResponse(request, 'snapshot.html',
|
||||
{'title': snapshot_module.title,
|
||||
'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.'))
|
||||
return redirect(reverse('snapshot:index'))
|
||||
|
||||
output = actions.superuser_run('snapshot', ['list'])
|
||||
snapshots = json.loads(output)
|
||||
|
||||
snapshot = None
|
||||
for s in snapshots:
|
||||
if s['number'] == number:
|
||||
snapshot = s
|
||||
|
||||
return TemplateResponse(request, 'snapshot_delete.html',
|
||||
{'title': _('Delete Snapshot'),
|
||||
'snapshot': snapshot})
|
||||
|
||||
|
||||
def rollback(request, number):
|
||||
"""Show confirmation to rollback to a snapshot."""
|
||||
if request.method == 'POST':
|
||||
actions.superuser_run('snapshot', ['rollback', number])
|
||||
messages.success(request, _('Set default subvolume for rollback.'))
|
||||
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 s in snapshots:
|
||||
if s['number'] == number:
|
||||
snapshot = s
|
||||
|
||||
return TemplateResponse(request, 'snapshot_rollback.html',
|
||||
{'title': _('Rollback to Snapshot'),
|
||||
'snapshot': snapshot})
|
||||
Loading…
x
Reference in New Issue
Block a user