Backups: allow testing the connection of ssh locations

Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Michael Pimmer 2018-11-26 03:19:54 +00:00 committed by James Valleroy
parent b6d1237c0c
commit 4eeceaa695
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
8 changed files with 173 additions and 30 deletions

View File

@ -27,9 +27,10 @@ import subprocess
import sys
import tarfile
from plinth.errors import ActionError
from plinth.modules.backups import MANIFESTS_FOLDER, REPOSITORY
TIMEOUT = 5
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
@ -38,7 +39,8 @@ def parse_arguments():
subparsers.add_parser(
'setup', help='Create repository if it does not already exist')
subparsers.add_parser('info', help='Show repository information')
info = subparsers.add_parser('info', help='Show repository information')
info.add_argument('--repository', help='Repository path', required=True)
subparsers.add_parser('list', help='List repository contents')
create = subparsers.add_parser('create', help='Create archive')
@ -94,9 +96,9 @@ def subcommand_setup(_):
subprocess.run(['borg', 'init', '--encryption', 'none', REPOSITORY])
def subcommand_info(_):
def subcommand_info(arguments):
"""Show repository information."""
subprocess.run(['borg', 'info', '--json', REPOSITORY], check=True)
run(['borg', 'info', '--json', arguments.repository])
def subcommand_list(_):
@ -189,7 +191,7 @@ def _get_apps_of_manifest(manifest):
elif type(manifest) is dict and 'apps' in manifest:
apps = manifest['apps']
else:
raise ActionError('Unknown manifest format')
raise RuntimeError('Unknown manifest format')
return apps
@ -236,6 +238,29 @@ def subcommand_restore_exported_archive(arguments):
break
def read_password():
"""Read the password from stdin."""
if sys.stdin.isatty():
return ''
else:
return ''.join(sys.stdin)
def run(cmd):
"""Pass provided passwords on to borg"""
env = dict(os.environ, BORG_RELOCATED_REPO_ACCESS_IS_OK='yes')
password = read_password()
timeout = None
if password:
env['SSHPASS'] = password
env['BORG_RSH'] = 'sshpass -e ssh -o StrictHostKeyChecking=no'
else:
# When no password is given ssh might ask for a password and get stuck.
# Use timeout to abort early.
timeout = TIMEOUT
subprocess.run(cmd, check=True, env=env, timeout=timeout)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()

View File

@ -59,8 +59,12 @@ def setup(helper, old_version=None):
helper.call('post', actions.superuser_run, 'backups', ['setup'])
def get_info():
output = actions.superuser_run('backups', ['info'])
def get_info(repository, password=None):
args = ['backups', ['info', '--repository', repository]]
kwargs = {}
if password is not None:
kwargs['input'] = password.encode()
output = actions.superuser_run(*args, **kwargs)
return json.loads(output)

View File

@ -76,38 +76,52 @@ class RestoreForm(forms.Form):
class UploadForm(forms.Form):
file = forms.FileField(label=_('Upload File'), required=True,
validators=[FileExtensionValidator(['gz'],
'Backup files have to be in .tar.gz format')],
help_text=_('Select the backup file you want to upload'))
file = forms.FileField(
label=_('Upload File'),
required=True,
validators=[FileExtensionValidator(['gz'],
'Backup files have to be in .tar.gz format')],
help_text=_('Select the backup file you want to upload'))
class CreateRepositoryForm(forms.Form):
repository = forms.CharField(
label=_('Repository path'), strip=True,
help_text=_('Path of the new repository.'))
label=_('SSH Repository Path'), strip=True,
help_text=_('Path of the new repository. Example: '
'<i>user@host/path/to/repo/</i>'))
ssh_password = forms.CharField(
label=_('SSH password'), strip=True,
help_text=_('Password of the SSH Server.<br />'
'If you have set up <a href="https://www.ssh.com/ssh/key/"'
'target="_blank">SSH key-based authentication</a> you can '
'omit the password.'),
required=False)
encryption = forms.ChoiceField(
label=_('Encryption'),
help_text=format_lazy(_('"Key in Repository" means that a '
'password-protected key is stored with the backup. <br />'
'<b>You need this password to restore a backup!</b>')),
help_text=format_lazy(
_('"Key in Repository" means that a '
'password-protected key is stored with the backup. <br />'
'<b>You need this password to restore a backup!</b>')),
choices=[('repokey', 'Key in Repository'), ('none', 'None')]
)
passphrase = forms.CharField(
label=_('Passphrase'),
help_text=_('Passphrase; Only needed when using encryption.'),
widget=forms.PasswordInput()
widget=forms.PasswordInput(),
required=False
)
confirm_passphrase = forms.CharField(
label=_('Confirm Passphrase'),
help_text=_('Repeat the passphrase.'),
widget=forms.PasswordInput()
widget=forms.PasswordInput(),
required=False
)
store_passphrase = forms.BooleanField(
label=_('Store passphrase on FreedomBox'),
help_text=format_lazy(_('Store the passphrase on your {box_name}.'
'<br />You need to store the passphrase if you want to run '
'recurrent backups.'), box_name=_(cfg.box_name)),
help_text=format_lazy(
_('Store the passphrase on your {box_name}.'
'<br />You need to store the passphrase if you want to run '
'recurrent backups.'), box_name=_(cfg.box_name)),
required=False
)

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of FreedomBox.
#
# 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 %}
<h3>{{ title }}</h3>
<form class="form" method="post" target="_blank">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Submit" %}"/>
<input type="submit" class="btn btn-secondary" value="Test Connection"
title="{% trans 'Test Connection to Repository' %}"
formaction="{% url 'backups:test-repository' %}" />
</form>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of FreedomBox.
#
# 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 %}
<h3>{{ title }}</h3>
{{ message }}
<div class="alert alert-warning" role="alert">
{{ error }}
</div>
{% endblock %}

View File

@ -22,7 +22,8 @@ from django.conf.urls import url
from .views import IndexView, CreateArchiveView, CreateRepositoryView, \
DeleteArchiveView, UploadArchiveView, ExportAndDownloadView, \
RepositoriesView, RestoreArchiveView, RestoreFromUploadView
RepositoriesView, RestoreArchiveView, RestoreFromUploadView, \
TestRepositoryView
urlpatterns = [
url(r'^sys/backups/$', IndexView.as_view(), name='index'),
@ -40,4 +41,6 @@ urlpatterns = [
RepositoriesView.as_view(), name='repositories'),
url(r'^sys/backups/repositories/create/$',
CreateRepositoryView.as_view(), name='create-repository'),
url(r'^sys/backups/repositories/test/$',
TestRepositoryView.as_view(), name='test-repository'),
]

View File

@ -40,10 +40,10 @@ from django.utils.translation import ugettext_lazy
from django.views.generic import View, FormView, TemplateView
from plinth import actions, kvstore
from plinth.errors import PlinthError
from plinth.errors import PlinthError, ActionError
from plinth.modules import backups, storage
from . import api, forms, SESSION_PATH_VARIABLE
from . import api, forms, SESSION_PATH_VARIABLE, REPOSITORY
from .decorators import delete_tmp_backup_file
logger = logging.getLogger(__name__)
@ -73,7 +73,7 @@ class IndexView(TemplateView):
context = super().get_context_data(**kwargs)
context['title'] = backups.name
context['description'] = backups.description
context['info'] = backups.get_info()
context['info'] = backups.get_info(REPOSITORY)
context['archives'] = backups.list_archives()
context['subsubmenu'] = subsubmenu
apps = api.get_all_apps_for_backup()
@ -282,12 +282,12 @@ class ExportAndDownloadView(View):
filename = "%s.tar.gz" % name
args = ['export-tar', '--name', name]
proc = actions.superuser_run('backups', args, run_in_background=True,
bufsize=1)
bufsize=1)
zipStream = ZipStream(proc.stdout, 'readline')
response = StreamingHttpResponse(zipStream,
content_type="application/x-gzip")
content_type="application/x-gzip")
response['Content-Disposition'] = 'attachment; filename="%s"' % \
filename
filename
return response
@ -309,7 +309,7 @@ class CreateRepositoryView(SuccessMessageMixin, FormView):
"""View to create a new repository."""
form_class = forms.CreateRepositoryForm
prefix = 'backups'
template_name = 'backups_form.html'
template_name = 'backups_create_repository.html'
success_url = reverse_lazy('backups:repositories')
success_message = _('Created new repository.')
@ -335,3 +335,25 @@ class CreateRepositoryView(SuccessMessageMixin, FormView):
repositories.append(new_repo)
kvstore.set('backups_repositories', json.dumps(repositories))
return super().form_valid(form)
class TestRepositoryView(TemplateView):
"""View to create a new repository."""
template_name = 'backups_test_repository.html'
def post(self, request):
context = self.get_context_data()
repository = request.POST['backups-repository']
ssh_password = request.POST['backups-ssh_password']
try:
info = backups.get_info(repository, password=ssh_password)
except ActionError as err:
if "subprocess.TimeoutExpired" in str(err):
msg = _("Server not reachable - try providing a password.")
context["error"] = msg
else:
context["error"] = str(err)
else:
context["message"] = info
return self.render_to_response(context)

View File

@ -146,7 +146,9 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
plinth.forms.LanguageSelectionFormMixin, forms.ModelForm):
"""When user info is changed, also updates LDAP user."""
ssh_keys = forms.CharField(
label=ugettext_lazy('SSH Keys'), required=False, widget=forms.Textarea,
label=ugettext_lazy('Authorized SSH Keys'),
required=False,
widget=forms.Textarea,
help_text=ugettext_lazy(
'Setting an SSH public key will allow this user to '
'securely log in to the system without using a '