mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
Backups, remote repositories: use object-oriented repositories
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
27fbc982c7
commit
cf6bbd6bba
@ -27,7 +27,7 @@ import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
from plinth.modules.backups import MANIFESTS_FOLDER, REPOSITORY
|
||||
from plinth.modules.backups import MANIFESTS_FOLDER
|
||||
|
||||
TIMEOUT = 5
|
||||
|
||||
@ -41,28 +41,14 @@ def parse_arguments():
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||
|
||||
subparsers.add_parser(
|
||||
setup = subparsers.add_parser(
|
||||
'setup', help='Create repository if it does not already exist')
|
||||
|
||||
info = subparsers.add_parser('info', help='Show repository information')
|
||||
info.add_argument('--repository', help='Repository path', required=True)
|
||||
|
||||
init = subparsers.add_parser('init', help='Initialize a repository')
|
||||
init.add_argument('--repository', help='Repository path', required=True)
|
||||
init.add_argument('--encryption', help='Enryption of the repository',
|
||||
required=True)
|
||||
|
||||
# TODO: ssh commands should be in SSH or a separate action script
|
||||
mount = subparsers.add_parser('mount', help='mount an ssh filesystem')
|
||||
mount.add_argument('--mountpoint', help='Local mountpoint', required=True)
|
||||
umount = subparsers.add_parser('umount',
|
||||
help='unmount an ssh filesystem')
|
||||
umount.add_argument('--mountpoint', help='Mountpoint to unmount',
|
||||
required=True)
|
||||
is_mounted = subparsers.add_parser('is-mounted',
|
||||
help='Check whether an sshfs is mouned')
|
||||
is_mounted.add_argument('--mountpoint', help='Mountpoint to check',
|
||||
required=True)
|
||||
info = subparsers.add_parser('info', help='Show repository information')
|
||||
|
||||
list_repo = subparsers.add_parser('list-repo',
|
||||
help='List repository contents')
|
||||
@ -77,16 +63,25 @@ def parse_arguments():
|
||||
|
||||
export_help = 'Export archive contents as tar on stdout'
|
||||
export_tar = subparsers.add_parser('export-tar', help=export_help)
|
||||
export_tar.add_argument('--name', help='Archive name)',
|
||||
required=True)
|
||||
|
||||
get_archive_apps = subparsers.add_parser(
|
||||
'get-archive-apps',
|
||||
help='Get list of apps included in archive')
|
||||
|
||||
# TODO: add parameters to missing commands (export etc)
|
||||
# SSH mount actions
|
||||
mount = subparsers.add_parser('mount', help='mount an ssh filesystem')
|
||||
mount.add_argument('--mountpoint', help='Local mountpoint', required=True)
|
||||
umount = subparsers.add_parser('umount',
|
||||
help='unmount an ssh filesystem')
|
||||
umount.add_argument('--mountpoint', help='Mountpoint to unmount',
|
||||
required=True)
|
||||
is_mounted = subparsers.add_parser('is-mounted',
|
||||
help='Check whether an sshfs is mouned')
|
||||
is_mounted.add_argument('--mountpoint', help='Mountpoint to check',
|
||||
required=True)
|
||||
|
||||
for cmd in [info, init, list_repo, create_archive, delete_archive,
|
||||
get_archive_apps, mount]:
|
||||
export_tar, get_archive_apps, mount, setup]:
|
||||
cmd.add_argument('--path', help='Repository or Archive path',
|
||||
required=False) # TODO: set required to True!
|
||||
cmd.add_argument('--ssh-keyfile', help='Path of private ssh key',
|
||||
@ -117,26 +112,26 @@ def parse_arguments():
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def subcommand_setup(_):
|
||||
def subcommand_setup(arguments):
|
||||
"""Create repository if it does not already exist."""
|
||||
# TODO: use init()
|
||||
env = get_env(arguments)
|
||||
try:
|
||||
subprocess.run(['borg', 'info', REPOSITORY], check=True)
|
||||
run(['borg', 'info', arguments.path], check=True, env=env)
|
||||
except:
|
||||
path = os.path.dirname(REPOSITORY)
|
||||
path = os.path.dirname(arguments.path)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
run(['borg', 'init', '--encryption', 'none', REPOSITORY])
|
||||
init(arguments.path, 'none', env=env)
|
||||
|
||||
|
||||
def init(repository, encryption, env=None):
|
||||
def init(path, encryption, env=None):
|
||||
"""Initialize a local or remote borg repository"""
|
||||
# TODO: verify that the repository does not exist?
|
||||
# TODO: does remote borg also create folders if they don't exist?
|
||||
if encryption != 'none' and 'BORG_PASSPHRASE' not in env:
|
||||
raise ValueError('No encryption passphrase provided')
|
||||
cmd = ['borg', 'init', '--encryption', encryption, repository]
|
||||
cmd = ['borg', 'init', '--encryption', encryption, path]
|
||||
run(cmd, env=env)
|
||||
|
||||
|
||||
@ -171,13 +166,13 @@ def get_sshfs_env(arguments):
|
||||
|
||||
def subcommand_init(arguments):
|
||||
env = get_env(arguments)
|
||||
init(arguments.repository, arguments.encryption, env=env)
|
||||
init(arguments.path, arguments.encryption, env=env)
|
||||
|
||||
|
||||
def subcommand_info(arguments):
|
||||
"""Show repository information."""
|
||||
env = get_env(arguments)
|
||||
run(['borg', 'info', '--json', arguments.repository], env=env)
|
||||
run(['borg', 'info', '--json', arguments.path], env=env)
|
||||
|
||||
|
||||
def subcommand_mount(arguments):
|
||||
@ -286,7 +281,9 @@ def _extract(archive_path, destination, locations=None):
|
||||
|
||||
def subcommand_export_tar(arguments):
|
||||
"""Export archive contents as tar stream on stdout."""
|
||||
run(['borg', 'export-tar', REPOSITORY + '::' + arguments.name, '-'])
|
||||
env = get_env(arguments)
|
||||
run(['borg', 'export-tar', arguments.path, '-'],
|
||||
env=env)
|
||||
|
||||
|
||||
def _read_archive_file(archive, filepath):
|
||||
@ -385,14 +382,14 @@ def read_password():
|
||||
return ''.join(sys.stdin)
|
||||
|
||||
|
||||
def run(cmd, env=None):
|
||||
def run(cmd, env=None, check=True):
|
||||
"""Wrap the command with ssh password or keyfile authentication"""
|
||||
# If the remote server asks for a password but no password is
|
||||
# provided, we get stuck at asking the password.
|
||||
timeout = None
|
||||
if env and 'BORG_RSH' in env and 'SSHPASS' not in env:
|
||||
timeout = TIMEOUT
|
||||
subprocess.run(cmd, check=True, env=env, timeout=timeout)
|
||||
subprocess.run(cmd, check=check, env=env, timeout=timeout)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -25,10 +25,8 @@ from django.utils.text import get_valid_filename
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth import actions, cfg
|
||||
from plinth.errors import ActionError
|
||||
from plinth.menu import main_menu
|
||||
from plinth.utils import format_lazy
|
||||
from .errors import BorgError, BorgRepositoryDoesNotExistError
|
||||
|
||||
from . import api
|
||||
|
||||
@ -45,30 +43,11 @@ description = [
|
||||
service = None
|
||||
|
||||
MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/'
|
||||
REPOSITORY = '/var/lib/freedombox/borgbackup'
|
||||
ROOT_REPOSITORY = '/var/lib/freedombox/borgbackup'
|
||||
ROOT_REPOSITORY_NAME = format_lazy(_('{box_name} storage'),
|
||||
box_name=cfg.box_name)
|
||||
# session variable name that stores when a backup file should be deleted
|
||||
SESSION_PATH_VARIABLE = 'fbx-backups-upload-path'
|
||||
# known errors that come up when remotely accessing a borg repository
|
||||
# 'errors' are error strings to look for in the stacktrace.
|
||||
ACCESS_PARAMS = ['ssh_keyfile', 'ssh_password', 'encryption_passphrase']
|
||||
# kvstore key for storing remote locations
|
||||
REMOTE_LOCATIONS_KEY = 'remote_locations'
|
||||
KNOWN_ERRORS = [{
|
||||
"errors": ["subprocess.TimeoutExpired"],
|
||||
"message": _("Connection refused - make sure you provided correct "
|
||||
"credentials and the server is running."),
|
||||
"raise_as": BorgError,
|
||||
},
|
||||
{
|
||||
"errors": ["Connection refused"],
|
||||
"message": _("Connection refused"),
|
||||
"raise_as": BorgError,
|
||||
},
|
||||
{
|
||||
"errors": ["not a valid repository", "does not exist"],
|
||||
"message": _("Repository not found"),
|
||||
"raise_as": BorgRepositoryDoesNotExistError,
|
||||
}]
|
||||
|
||||
|
||||
def init():
|
||||
@ -80,67 +59,8 @@ def init():
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
helper.install(managed_packages)
|
||||
helper.call('post', actions.superuser_run, 'backups', ['setup'])
|
||||
|
||||
|
||||
def get_info(repository, access_params=None):
|
||||
cmd = ['info', '--repository', repository]
|
||||
output = run(cmd, access_params)
|
||||
return json.loads(output)
|
||||
|
||||
|
||||
def list_archives(repository, access_params=None):
|
||||
try:
|
||||
output = run(['list-repo', '--path', repository], access_params)
|
||||
except ActionError as err:
|
||||
reraise_known_error(err)
|
||||
else:
|
||||
return json.loads(output)['archives']
|
||||
|
||||
|
||||
def get_root_location_content():
|
||||
"""
|
||||
Get information about the root backup location in the same format
|
||||
that the remote repositories use
|
||||
"""
|
||||
return {
|
||||
'name': format_lazy(_('{box_name} storage'), box_name=cfg.box_name),
|
||||
'mounted': True,
|
||||
'archives': list_archives(REPOSITORY),
|
||||
'type': 'rootfs',
|
||||
}
|
||||
|
||||
|
||||
def get_archive(name):
|
||||
# TODO: can't we get this archive directly?
|
||||
for archive in list_archives():
|
||||
if archive['name'] == name:
|
||||
return archive
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def reraise_known_error(err):
|
||||
"""Look whether the caught error is known and re-raise it accordingly"""
|
||||
caught_error = str(err)
|
||||
for known_error in KNOWN_ERRORS:
|
||||
for error in known_error["errors"]:
|
||||
if error in caught_error:
|
||||
raise known_error["raise_as"](known_error["message"])
|
||||
else:
|
||||
raise err
|
||||
|
||||
|
||||
def test_connection(repository, access_params=None):
|
||||
"""
|
||||
Test connecting to a local or remote borg repository.
|
||||
Tries to detect (and throw) some known ssh or borg errors.
|
||||
Returns 'borg info' information otherwise.
|
||||
"""
|
||||
try:
|
||||
return get_info(repository, access_params)
|
||||
except ActionError as err:
|
||||
reraise_known_error(err)
|
||||
helper.call('post', actions.superuser_run, 'backups', ['setup', '--path',
|
||||
ROOT_REPOSITORY])
|
||||
|
||||
|
||||
def _backup_handler(packet):
|
||||
@ -168,32 +88,6 @@ def _backup_handler(packet):
|
||||
paths)
|
||||
|
||||
|
||||
def create_archive(name, app_names, access_params=None):
|
||||
api.backup_apps(_backup_handler, app_names, name,
|
||||
access_params=access_params)
|
||||
|
||||
|
||||
def create_repository(repository, encryption, access_params=None):
|
||||
cmd = ['init', '--repository', repository, '--encryption', encryption]
|
||||
run(cmd, access_params=access_params)
|
||||
|
||||
|
||||
def delete_archive(path):
|
||||
actions.superuser_run('backups', ['delete-archive', '--path', path])
|
||||
|
||||
|
||||
def get_archive_path(archive_name):
|
||||
"""Get path of an archive"""
|
||||
return "::".join([REPOSITORY, archive_name])
|
||||
|
||||
|
||||
def get_archive_apps(path):
|
||||
"""Get list of apps included in an archive."""
|
||||
output = actions.superuser_run('backups',
|
||||
['get-archive-apps', '--path', path])
|
||||
return output.splitlines()
|
||||
|
||||
|
||||
def get_exported_archive_apps(path):
|
||||
"""Get list of apps included in exported archive file."""
|
||||
arguments = ['get-exported-archive-apps', '--path', path]
|
||||
@ -229,34 +123,3 @@ def restore(archive_path, apps=None):
|
||||
"""Restore files from a backup archive."""
|
||||
api.restore_apps(_restore_archive_handler, app_names=apps,
|
||||
create_subvolume=False, backup_file=archive_path)
|
||||
|
||||
|
||||
def get_args(params):
|
||||
args = []
|
||||
kwargs = {}
|
||||
if 'ssh_password' in params and params['ssh_password'] is not None:
|
||||
kwargs['input'] = params['ssh_password'].encode()
|
||||
if 'ssh_keyfile' in params and params['ssh_keyfile'] is not None:
|
||||
args += ['--ssh-keyfile', params['ssh_keyfile']]
|
||||
if 'encryption_passphrase' in params and \
|
||||
params['encryption_passphrase'] is not None:
|
||||
args += ['--encryption-passphrase', params['encryption_passphrase']]
|
||||
return (args, kwargs)
|
||||
|
||||
|
||||
def run(arguments, access_params=None, superuser=True):
|
||||
"""Run a given command, appending (kw)args as necessary"""
|
||||
command = 'backups'
|
||||
kwargs = {}
|
||||
if access_params:
|
||||
for key in access_params.keys():
|
||||
if key not in ACCESS_PARAMS:
|
||||
raise ValueError('Unknown access parameter: %s' % key)
|
||||
|
||||
append_arguments, kwargs = get_args(access_params)
|
||||
arguments += append_arguments
|
||||
|
||||
if superuser:
|
||||
return actions.superuser_run(command, arguments, **kwargs)
|
||||
else:
|
||||
return actions.run(command, arguments, **kwargs)
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
"""
|
||||
Manage borg backup locations
|
||||
"""
|
||||
|
||||
|
||||
class Location(object):
|
||||
def __init__(self, uuid):
|
||||
pass
|
||||
68
plinth/modules/backups/network_storage.py
Normal file
68
plinth/modules/backups/network_storage.py
Normal file
@ -0,0 +1,68 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
"""
|
||||
Manage remote (network) storage storages in plinths' KVStore.
|
||||
"""
|
||||
|
||||
import json
|
||||
from uuid import uuid1
|
||||
|
||||
from plinth import kvstore
|
||||
|
||||
# kvstore key for network storage
|
||||
NETWORK_STORAGE_KEY = 'network_storage'
|
||||
REQUIRED_FIELDS = ['path', 'storage_type', 'added_by_module']
|
||||
|
||||
|
||||
def get_storages(storage_type=None):
|
||||
"""Get list of network storage storages"""
|
||||
storages = kvstore.get_default(NETWORK_STORAGE_KEY, [])
|
||||
if storages:
|
||||
storages = json.loads(storages)
|
||||
if storage_type:
|
||||
storages = [storage for storage in storages if 'type' in storage
|
||||
and storage['type'] == storage_type]
|
||||
return storages
|
||||
|
||||
|
||||
def get(uuid):
|
||||
storages = get_storages()
|
||||
return list(filter(lambda storage: storage['uuid'] == uuid,
|
||||
storages))[0]
|
||||
|
||||
|
||||
def update_or_add(storage):
|
||||
"""Update an existing or create a new network location"""
|
||||
for field in REQUIRED_FIELDS:
|
||||
assert field in storage
|
||||
storages = get_storages()
|
||||
if 'uuid' in storage:
|
||||
# Replace the existing storage
|
||||
storages = [_storage if _storage['uuid'] != storage['uuid'] else
|
||||
storage for _storage in storages]
|
||||
else:
|
||||
storage['uuid'] = str(uuid1())
|
||||
storages.append(storage)
|
||||
kvstore.set(NETWORK_STORAGE_KEY, json.dumps(storages))
|
||||
|
||||
|
||||
def delete(uuid):
|
||||
"""Remove a network storage from kvstore"""
|
||||
storages = get_storages()
|
||||
storages = list(filter(lambda storage: storage['uuid'] != uuid,
|
||||
storages))
|
||||
kvstore.set(NETWORK_STORAGE_KEY, json.dumps(storages))
|
||||
@ -1,180 +0,0 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
"""
|
||||
Manage remote storage locations
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from uuid import uuid1
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from plinth import kvstore
|
||||
from plinth.errors import ActionError
|
||||
|
||||
from . import sshfs, list_archives, reraise_known_error, REMOTE_LOCATIONS_KEY
|
||||
from .errors import BorgError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MOUNTPOINT = '/media/'
|
||||
|
||||
|
||||
def add(path, repotype, encryption, access_params, store_passwords, added_by):
|
||||
locations = get_locations()
|
||||
location = {
|
||||
'uuid': str(uuid1()),
|
||||
'path': path,
|
||||
'type': repotype,
|
||||
'encryption': encryption,
|
||||
'added_by': added_by
|
||||
}
|
||||
if store_passwords:
|
||||
if 'encryption_passphrase' in access_params:
|
||||
location['encryption_passphrase'] = \
|
||||
access_params['encryption_passphrase']
|
||||
if 'ssh_password' in access_params:
|
||||
location['ssh_password'] = access_params['ssh_password']
|
||||
locations.append(location)
|
||||
kvstore.set(REMOTE_LOCATIONS_KEY, json.dumps(locations))
|
||||
|
||||
|
||||
def delete(uuid):
|
||||
"""Umount a location, remove it from kvstore and unlink the mountpoint"""
|
||||
locations = get_locations()
|
||||
location = get_location(uuid)
|
||||
mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
|
||||
locations = list(filter(lambda location: location['uuid'] != uuid,
|
||||
locations))
|
||||
kvstore.set(REMOTE_LOCATIONS_KEY, json.dumps(locations))
|
||||
if os.path.exists(mountpoint):
|
||||
try:
|
||||
sshfs.umount(mountpoint)
|
||||
except ActionError:
|
||||
pass
|
||||
try:
|
||||
os.unlink(mountpoint)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
|
||||
|
||||
def get_locations_content(uuid=None):
|
||||
"""
|
||||
Get archives of one or all locations.
|
||||
returns: {
|
||||
uuid: {
|
||||
'path': path,
|
||||
'type': type,
|
||||
'archives': [],
|
||||
'error': error_message,
|
||||
}
|
||||
}
|
||||
"""
|
||||
locations = {}
|
||||
for location in get_locations():
|
||||
mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
|
||||
new_location = {
|
||||
'name': location['path'],
|
||||
'mounted': uuid_is_mounted(location['uuid']),
|
||||
}
|
||||
if new_location['mounted']:
|
||||
try:
|
||||
new_location['archives'] = list_archives(mountpoint)
|
||||
except BorgError as err:
|
||||
new_location['error'] = str(err)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
new_location['error'] = _("Access failed")
|
||||
locations[location['uuid']] = new_location
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
def get_locations(location_type=None):
|
||||
"""Get list of all locations"""
|
||||
# TODO: hold locations in memory?
|
||||
locations = kvstore.get_default(REMOTE_LOCATIONS_KEY, [])
|
||||
if locations:
|
||||
locations = json.loads(locations)
|
||||
if location_type:
|
||||
locations = [location for location in locations if 'type' in location
|
||||
and location['type'] == location_type]
|
||||
return locations
|
||||
|
||||
|
||||
def get_location(uuid):
|
||||
locations = get_locations()
|
||||
return list(filter(lambda location: location['uuid'] == uuid,
|
||||
locations))[0]
|
||||
|
||||
|
||||
def _mount_locations(uuid=None):
|
||||
locations = get_locations(location_type='ssh')
|
||||
for location in locations:
|
||||
_mount_location(location)
|
||||
|
||||
|
||||
def _mount_location(location):
|
||||
# TODO: shouldn't I just store and query the access_params as they are?
|
||||
# but encryption_passphrase is not an ssh access_param..
|
||||
mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
|
||||
is_mounted = False
|
||||
if sshfs.is_mounted(mountpoint):
|
||||
is_mounted = True
|
||||
else:
|
||||
access_params = _get_access_params(location)
|
||||
# TODO: use actual feedback of sshfs.mount
|
||||
try:
|
||||
sshfs.mount(location['path'], mountpoint, access_params)
|
||||
except Exception as err:
|
||||
reraise_known_error(err)
|
||||
is_mounted = True
|
||||
return is_mounted
|
||||
|
||||
|
||||
def _umount_location(location):
|
||||
mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
|
||||
return sshfs.umount(mountpoint)
|
||||
|
||||
|
||||
def _get_access_params(location):
|
||||
keys = ['encryption_passphrase', 'ssh_keyfile', 'ssh_password']
|
||||
access_params = {key: location[key] for key in keys if key in location}
|
||||
if location['type'] == 'ssh':
|
||||
if 'ssh_keyfile' not in location and 'ssh_password' not in \
|
||||
location:
|
||||
raise ValueError('Missing credentials')
|
||||
return access_params
|
||||
|
||||
|
||||
def mount_uuid(uuid):
|
||||
location = get_location(uuid)
|
||||
mounted = False
|
||||
if location:
|
||||
mounted = _mount_location(location)
|
||||
return mounted
|
||||
|
||||
|
||||
def umount_uuid(uuid):
|
||||
location = get_location(uuid)
|
||||
return _umount_location(location)
|
||||
|
||||
|
||||
def uuid_is_mounted(uuid):
|
||||
mountpoint = os.path.join(MOUNTPOINT, uuid)
|
||||
return sshfs.is_mounted(mountpoint)
|
||||
266
plinth/modules/backups/repository.py
Normal file
266
plinth/modules/backups/repository.py
Normal file
@ -0,0 +1,266 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
"""
|
||||
Remote and local Borg backup repositories
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth import actions
|
||||
from plinth.errors import ActionError
|
||||
|
||||
from . import api, network_storage, _backup_handler, ROOT_REPOSITORY_NAME
|
||||
from .errors import BorgError, BorgRepositoryDoesNotExistError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SSHFS_MOUNTPOINT = '/media/'
|
||||
# known errors that come up when remotely accessing a borg repository
|
||||
# 'errors' are error strings to look for in the stacktrace.
|
||||
KNOWN_ERRORS = [{
|
||||
"errors": ["subprocess.TimeoutExpired"],
|
||||
"message": _("Connection refused - make sure you provided correct "
|
||||
"credentials and the server is running."),
|
||||
"raise_as": BorgError,
|
||||
},
|
||||
{
|
||||
"errors": ["Connection refused"],
|
||||
"message": _("Connection refused"),
|
||||
"raise_as": BorgError,
|
||||
},
|
||||
{
|
||||
"errors": ["not a valid repository", "does not exist"],
|
||||
"message": _("Repository not found"),
|
||||
"raise_as": BorgRepositoryDoesNotExistError,
|
||||
}]
|
||||
|
||||
|
||||
class BorgRepository(object):
|
||||
"""Borg repository on the root filesystem"""
|
||||
command = 'backups'
|
||||
storage_type = 'root'
|
||||
name = ROOT_REPOSITORY_NAME
|
||||
is_mounted = True
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def get_info(self):
|
||||
output = self._run(['info', '--path', self.path])
|
||||
return json.loads(output)
|
||||
|
||||
def list_archives(self):
|
||||
output = self._run(['list-repo', '--path', self.path])
|
||||
return json.loads(output)['archives']
|
||||
|
||||
def get_view_content(self):
|
||||
"""Get archives with additional information as needed by the view"""
|
||||
return {
|
||||
'name': self.name,
|
||||
'mounted': self.is_mounted,
|
||||
'archives': self.list_archives(),
|
||||
'type': self.storage_type,
|
||||
}
|
||||
|
||||
def delete_archive(self, archive_name):
|
||||
archive_path = self.get_archive_path(archive_name)
|
||||
self._run(['delete-archive', '--path', archive_path])
|
||||
|
||||
def remove(self):
|
||||
"""Remove a borg repository"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_archive(self, app_names, archive_name):
|
||||
api.backup_apps(_backup_handler, app_names=app_names,
|
||||
label=archive_name),
|
||||
|
||||
def download_archive(self, name):
|
||||
pass
|
||||
|
||||
def get_archive(self, name):
|
||||
# TODO: can't we get this archive directly?
|
||||
for archive in self.list_archives():
|
||||
if archive['name'] == name:
|
||||
return archive
|
||||
|
||||
return None
|
||||
|
||||
def get_archive_apps(self, archive_name):
|
||||
"""Get list of apps included in an archive."""
|
||||
archive_path = self.get_archive_path(archive_name)
|
||||
output = self._run(['get-archive-apps', '--path', archive_path])
|
||||
return output.splitlines()
|
||||
|
||||
def restore_archive(self):
|
||||
pass
|
||||
|
||||
def get_archive_path(self, archive_name):
|
||||
return "::".join(self.path, archive_name)
|
||||
|
||||
def _run(self, arguments, superuser=True):
|
||||
"""Run a backups action script command."""
|
||||
try:
|
||||
if superuser:
|
||||
return actions.superuser_run(self.command, arguments)
|
||||
else:
|
||||
return actions.run(self.command, arguments)
|
||||
except ActionError as err:
|
||||
self.reraise_known_error(err)
|
||||
|
||||
def reraise_known_error(self, err):
|
||||
"""Look whether the caught error is known and reraise it accordingly"""
|
||||
caught_error = str(err)
|
||||
for known_error in KNOWN_ERRORS:
|
||||
for error in known_error["errors"]:
|
||||
if error in caught_error:
|
||||
raise known_error["raise_as"](known_error["message"])
|
||||
else:
|
||||
raise err
|
||||
|
||||
|
||||
class SshBorgRepository(BorgRepository):
|
||||
KNOWN_CREDENTIALS = ['ssh_keyfile', 'ssh_password',
|
||||
'encryption_passphrase']
|
||||
storage_type = 'ssh'
|
||||
|
||||
def __init__(self, uuid=None, path=None, credentials=None, **kwargs):
|
||||
"""
|
||||
Provide a uuid to instanciate an existing repository,
|
||||
or 'path' and 'credentials' for a new repository.
|
||||
"""
|
||||
if uuid:
|
||||
self.uuid = uuid
|
||||
# If all data are given, instanciate right away.
|
||||
if path and credentials:
|
||||
self.path = path
|
||||
self.credentials = credentials
|
||||
else:
|
||||
self._load_from_kvstore()
|
||||
# No uuid given: new instance.
|
||||
elif path and credentials:
|
||||
self.path = path
|
||||
self.credentials = credentials
|
||||
else:
|
||||
raise ValueError('Invalid arguments.')
|
||||
|
||||
@property
|
||||
def mountpoint(self):
|
||||
return os.path.join(SSHFS_MOUNTPOINT, self.uuid)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.path
|
||||
|
||||
@property
|
||||
def is_mounted(self):
|
||||
output = self._run(['is-mounted', '--mountpoint', self.mountpoint])
|
||||
return json.loads(output)
|
||||
|
||||
def _load_from_kvstore(self):
|
||||
storage = network_storage.get(self.uuid)
|
||||
self.credentials = storage['credentials']
|
||||
self.path = storage['path']
|
||||
|
||||
def _get_network_storage_format(self):
|
||||
storage = {
|
||||
'path': self.path,
|
||||
'credentials': self.credentials,
|
||||
'storage_type': self.storage_type,
|
||||
'added_by_module': 'backups'
|
||||
}
|
||||
if hasattr(self, 'uuid'):
|
||||
storage['uuid'] = self.uuid
|
||||
return storage
|
||||
|
||||
def create_archive(self, app_names, archive_name):
|
||||
api.backup_apps(_backup_handler, app_names=app_names,
|
||||
label=archive_name, credentials=self.credentials)
|
||||
|
||||
def create_repository(self, encryption):
|
||||
cmd = ['init', '--path', self.path, '--encryption', encryption]
|
||||
self._run(cmd)
|
||||
|
||||
def save(self):
|
||||
storage = self._get_network_storage_format()
|
||||
self.uuid = network_storage.update_or_add(storage)
|
||||
|
||||
def mount(self):
|
||||
cmd = ['mount', '--mountpoint', self.mountpoint, '--path', self.path]
|
||||
self._run(cmd)
|
||||
|
||||
def umount(self):
|
||||
self._run(['umount', '--mountpoint', self.mountpoint],
|
||||
use_credentials=False)
|
||||
|
||||
def remove(self):
|
||||
"""Remove a repository from the kvstore and delete its mountpoint"""
|
||||
network_storage.delete(self.uuid)
|
||||
try:
|
||||
if os.path.exists(self.mountpoint):
|
||||
try:
|
||||
self.umount()
|
||||
except ActionError:
|
||||
pass
|
||||
if not os.listdir(self.mountpoint):
|
||||
os.rmdir(self.mountpoint)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
|
||||
def _run(self, arguments, superuser=True, use_credentials=True):
|
||||
"""Run a backups action script command.
|
||||
|
||||
This automatically passes on self.credentials to the backups script
|
||||
via environment variables or input, except if you set use_credentials
|
||||
to False.
|
||||
"""
|
||||
for key in self.credentials.keys():
|
||||
if key not in self.KNOWN_CREDENTIALS:
|
||||
raise ValueError('Unknown credentials: %s' % key)
|
||||
|
||||
kwargs = {}
|
||||
if use_credentials:
|
||||
if 'ssh_password' in self.credentials and \
|
||||
self.credentials['ssh_password'] is not None:
|
||||
kwargs['input'] = self.credentials['ssh_password'].encode()
|
||||
if 'ssh_keyfile' in self.credentials and \
|
||||
self.credentials['ssh_keyfile'] is not None:
|
||||
arguments += ['--ssh-keyfile', self.credentials['ssh_keyfile']]
|
||||
if 'encryption_passphrase' in self.credentials and \
|
||||
self.credentials['encryption_passphrase'] is not None:
|
||||
arguments += ['--encryption-passphrase',
|
||||
self.credentials['encryption_passphrase']]
|
||||
|
||||
try:
|
||||
if superuser:
|
||||
return actions.superuser_run(self.command, arguments, **kwargs)
|
||||
else:
|
||||
return actions.run(self.command, arguments, **kwargs)
|
||||
except ActionError as err:
|
||||
self.reraise_known_error(err)
|
||||
|
||||
|
||||
def get_ssh_repositories():
|
||||
"""Get all SSH Repositories including the archive content"""
|
||||
repositories = {}
|
||||
for storage in network_storage.get_storages():
|
||||
repository = SshBorgRepository(**storage)
|
||||
repositories[storage['uuid']] = repository.get_view_content()
|
||||
return repositories
|
||||
@ -1,38 +0,0 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Utilities to work with sshfs.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from plinth.modules.backups import run
|
||||
|
||||
|
||||
def mount(remote_path, mountpoint, access_params):
|
||||
run(['mount', '--mountpoint', mountpoint, '--path', remote_path],
|
||||
access_params)
|
||||
|
||||
|
||||
def umount(mountpoint):
|
||||
run(['umount', '--mountpoint', mountpoint])
|
||||
|
||||
|
||||
def is_mounted(mountpoint):
|
||||
output = run(['is-mounted', '--mountpoint', mountpoint])
|
||||
return json.loads(output)
|
||||
@ -52,17 +52,17 @@
|
||||
|
||||
<h3>{% trans 'Existing backups' %}</h3>
|
||||
|
||||
{% include "backups_location.inc" with location=root_location uuid='root' editable=False %}
|
||||
{% include "backups_repository.inc" with repository=root_repository uuid='root' editable=False %}
|
||||
|
||||
{% for uuid,location in remote_locations.items %}
|
||||
{% include "backups_location.inc" with editable=True %}
|
||||
{% for uuid,repository in ssh_repositories.items %}
|
||||
{% include "backups_repository.inc" with editable=True %}
|
||||
{% endfor %}
|
||||
|
||||
<br />
|
||||
|
||||
<a title="{% trans 'Create new repository' %}"
|
||||
role="button" class="btn btn-primary"
|
||||
href="{% url 'backups:location-add' %}">
|
||||
href="{% url 'backups:repository-add' %}">
|
||||
{% trans '+ Add Remote Repository' %}
|
||||
</a>
|
||||
|
||||
|
||||
@ -25,16 +25,16 @@
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
{{ location.name }}
|
||||
{{ repository.name }}
|
||||
|
||||
{% if editable %}
|
||||
|
||||
{% if location.mounted %}
|
||||
{% if repository.mounted %}
|
||||
|
||||
<!-- With GET redirects the browser URL would be pointing to the
|
||||
redirected page - use POST instead.
|
||||
-->
|
||||
<form action="{% url 'backups:location-umount' uuid %}" method="POST"
|
||||
<form action="{% url 'backups:repository-umount' uuid %}" method="POST"
|
||||
class="inline-block" >
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-default"
|
||||
@ -45,7 +45,7 @@
|
||||
|
||||
{% else %}
|
||||
|
||||
<form action="{% url 'backups:location-mount' uuid %}" method="POST"
|
||||
<form action="{% url 'backups:repository-mount' uuid %}" method="POST"
|
||||
class="inline-block" >
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-default"
|
||||
@ -57,8 +57,8 @@
|
||||
{% endif %}
|
||||
|
||||
<a title="{% trans 'Remove Location. This will not delete the remote backup.' %}"
|
||||
role="button" class="location-remove btn btn-sm btn-default"
|
||||
href="{% url 'backups:location-remove' uuid %}">
|
||||
role="button" class="repository-remove btn btn-sm btn-default"
|
||||
href="{% url 'backups:repository-remove' uuid %}">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true">
|
||||
</a>
|
||||
|
||||
@ -71,9 +71,9 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% if location.mounted %}
|
||||
{% if repository.mounted %}
|
||||
|
||||
{% for archive in location.archives %}
|
||||
{% for archive in repository.archives %}
|
||||
<tr id="archive-{{ archive.name }}" class="archive">
|
||||
<td class="archive-name">{{ archive.name }}</td>
|
||||
<td class="archive-operations">
|
||||
@ -94,7 +94,7 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if not location.archives %}
|
||||
{% if not repository.archives %}
|
||||
<p>{% trans 'No archives currently exist.' %}</p>
|
||||
{% endif %}
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
value="{% trans "Submit" %}"/>
|
||||
<input type="submit" class="btn btn-secondary" value="Test Connection"
|
||||
title="{% trans 'Test Connection to Repository' %}"
|
||||
formaction="{% url 'backups:location-test' %}" />
|
||||
formaction="{% url 'backups:repository-test' %}" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@ -27,7 +27,7 @@
|
||||
<p>
|
||||
<b>
|
||||
{% trans "Are you sure that you want to remove the repository" %}
|
||||
{{ location.path }}?
|
||||
{{ repository.path }}?
|
||||
</b>
|
||||
</p>
|
||||
{% blocktrans %}
|
||||
@ -42,7 +42,7 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="submit" class="btn btn-danger"
|
||||
value="{% blocktrans trimmed with path=location.path %}
|
||||
value="{% blocktrans trimmed with path=repository.path %}
|
||||
Remove Repository
|
||||
{% endblocktrans %}"/>
|
||||
<a class="abort btn btn-sm btn-default"
|
||||
@ -20,10 +20,10 @@ URLs for the backups module.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import IndexView, CreateArchiveView, AddLocationView, \
|
||||
DeleteArchiveView, ExportAndDownloadView, RemoveLocationView, \
|
||||
mount_location, umount_location, UploadArchiveView, \
|
||||
RestoreArchiveView, RestoreFromUploadView, TestLocationView
|
||||
from .views import IndexView, CreateArchiveView, AddRepositoryView, \
|
||||
DeleteArchiveView, ExportAndDownloadView, RemoveRepositoryView, \
|
||||
mount_repository, umount_repository, UploadArchiveView, \
|
||||
RestoreArchiveView, RestoreFromUploadView, TestRepositoryView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sys/backups/$', IndexView.as_view(), name='index'),
|
||||
@ -37,14 +37,14 @@ urlpatterns = [
|
||||
RestoreArchiveView.as_view(), name='restore-archive'),
|
||||
url(r'^sys/backups/restore-from-upload/$',
|
||||
RestoreFromUploadView.as_view(), name='restore-from-upload'),
|
||||
url(r'^sys/backups/locations/add$',
|
||||
AddLocationView.as_view(), name='location-add'),
|
||||
url(r'^sys/backups/locations/test/$',
|
||||
TestLocationView.as_view(), name='location-test'),
|
||||
url(r'^sys/backups/locations/delete/(?P<uuid>[^/]+)/$',
|
||||
RemoveLocationView.as_view(), name='location-remove'),
|
||||
url(r'^sys/backups/locations/mount/(?P<uuid>[^/]+)/$',
|
||||
mount_location, name='location-mount'),
|
||||
url(r'^sys/backups/locations/umount/(?P<uuid>[^/]+)/$',
|
||||
umount_location, name='location-umount'),
|
||||
url(r'^sys/backups/repositories/add$',
|
||||
AddRepositoryView.as_view(), name='repository-add'),
|
||||
url(r'^sys/backups/repositories/test/$',
|
||||
TestRepositoryView.as_view(), name='repository-test'),
|
||||
url(r'^sys/backups/repositories/delete/(?P<uuid>[^/]+)/$',
|
||||
RemoveRepositoryView.as_view(), name='repository-remove'),
|
||||
url(r'^sys/backups/repositories/mount/(?P<uuid>[^/]+)/$',
|
||||
mount_repository, name='repository-mount'),
|
||||
url(r'^sys/backups/repositories/umount/(?P<uuid>[^/]+)/$',
|
||||
umount_repository, name='repository-umount'),
|
||||
]
|
||||
|
||||
@ -41,7 +41,8 @@ from plinth import actions
|
||||
from plinth.errors import PlinthError, ActionError
|
||||
from plinth.modules import backups, storage
|
||||
|
||||
from . import api, forms, SESSION_PATH_VARIABLE, REPOSITORY, remote_locations
|
||||
from . import api, forms, SESSION_PATH_VARIABLE, ROOT_REPOSITORY
|
||||
from .repository import BorgRepository, SshBorgRepository, get_ssh_repositories
|
||||
from .decorators import delete_tmp_backup_file
|
||||
from .errors import BorgRepositoryDoesNotExistError
|
||||
|
||||
@ -72,9 +73,9 @@ class IndexView(TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = backups.name
|
||||
context['description'] = backups.description
|
||||
context['info'] = backups.get_info(REPOSITORY)
|
||||
context['root_location'] = backups.get_root_location_content()
|
||||
context['remote_locations'] = remote_locations.get_locations_content()
|
||||
root_repository = BorgRepository(ROOT_REPOSITORY)
|
||||
context['root_repository'] = root_repository.get_view_content()
|
||||
context['ssh_repositories'] = get_ssh_repositories()
|
||||
context['subsubmenu'] = subsubmenu
|
||||
return context
|
||||
|
||||
@ -290,13 +291,13 @@ class ExportAndDownloadView(View):
|
||||
return response
|
||||
|
||||
|
||||
class AddLocationView(SuccessMessageMixin, FormView):
|
||||
"""View to create a new remote backup location."""
|
||||
class AddRepositoryView(SuccessMessageMixin, FormView):
|
||||
"""View to create a new remote backup repository."""
|
||||
form_class = forms.AddRepositoryForm
|
||||
prefix = 'backups'
|
||||
template_name = 'backups_add_location.html'
|
||||
template_name = 'backups_repository_add.html'
|
||||
success_url = reverse_lazy('backups:index')
|
||||
success_message = _('Added new location.')
|
||||
success_message = _('Added new repository.')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Return additional context for rendering the template."""
|
||||
@ -307,47 +308,42 @@ class AddLocationView(SuccessMessageMixin, FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Restore files from the archive on valid form submission."""
|
||||
repository = form.cleaned_data['repository']
|
||||
encryption = form.cleaned_data['encryption']
|
||||
encryption_passphrase = form.cleaned_data['encryption_passphrase']
|
||||
ssh_password = form.cleaned_data['ssh_password']
|
||||
store_passwords = form.cleaned_data['store_passwords']
|
||||
# TODO: add ssh_keyfile
|
||||
# ssh_keyfile = form.cleaned_data['ssh_keyfile']
|
||||
path = form.cleaned_data['repository']
|
||||
credentials = {}
|
||||
if form.cleaned_data['store_passwords']:
|
||||
encryption_passphrase = form.cleaned_data['encryption_passphrase']
|
||||
if encryption_passphrase:
|
||||
credentials['encryption_passphrase'] = encryption_passphrase
|
||||
if form.cleaned_data['ssh_password']:
|
||||
credentials['ssh_password'] = form.cleaned_data['ssh_password']
|
||||
# TODO: add ssh_keyfile
|
||||
# ssh_keyfile = form.cleaned_data['ssh_keyfile']
|
||||
|
||||
repository = SshBorgRepository(path=path, credentials=credentials)
|
||||
|
||||
access_params = {}
|
||||
if encryption_passphrase:
|
||||
access_params['encryption_passphrase'] = encryption_passphrase
|
||||
if ssh_password:
|
||||
access_params['ssh_password'] = ssh_password
|
||||
"""
|
||||
if ssh_keyfile:
|
||||
access_params['ssh_keyfile'] = ssh_keyfile
|
||||
"""
|
||||
remote_locations.add(repository, 'ssh', encryption, access_params,
|
||||
store_passwords, 'backups')
|
||||
# Create the borg repository if it doesn't exist
|
||||
try:
|
||||
backups.test_connection(repository, access_params)
|
||||
repository.get_info()
|
||||
except BorgRepositoryDoesNotExistError:
|
||||
backups.create_repository(repository, encryption,
|
||||
access_params=access_params)
|
||||
repository.create_repository(form.cleaned_data['encryption'])
|
||||
repository.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TestLocationView(TemplateView):
|
||||
class TestRepositoryView(TemplateView):
|
||||
"""View to create a new repository."""
|
||||
template_name = 'backups_test_location.html'
|
||||
template_name = 'backups_repository_test.html'
|
||||
|
||||
def post(self, request):
|
||||
# TODO: add support for borg encryption and ssh keyfile
|
||||
context = self.get_context_data()
|
||||
repository = request.POST['backups-repository']
|
||||
access_params = {
|
||||
credentials = {
|
||||
'ssh_password': request.POST['backups-ssh_password'],
|
||||
}
|
||||
repository = SshBorgRepository(path=request.POST['backups-repository'],
|
||||
credentials=credentials)
|
||||
|
||||
try:
|
||||
repo_info = backups.test_connection(repository, access_params)
|
||||
repo_info = repository.get_info()
|
||||
context["message"] = repo_info
|
||||
except ActionError as err:
|
||||
context["error"] = str(err)
|
||||
@ -355,39 +351,42 @@ class TestLocationView(TemplateView):
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class RemoveLocationView(SuccessMessageMixin, TemplateView):
|
||||
class RemoveRepositoryView(SuccessMessageMixin, TemplateView):
|
||||
"""View to delete an archive."""
|
||||
template_name = 'backups_remove_repository.html'
|
||||
template_name = 'backups_repository_remove.html'
|
||||
|
||||
def get_context_data(self, uuid, **kwargs):
|
||||
"""Return additional context for rendering the template."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _('Remove Repository')
|
||||
context['location'] = remote_locations.get_location(uuid)
|
||||
context['repository'] = SshBorgRepository(uuid=uuid)
|
||||
return context
|
||||
|
||||
def post(self, request, uuid):
|
||||
"""Delete the archive."""
|
||||
remote_locations.delete(uuid)
|
||||
repository = SshBorgRepository(uuid)
|
||||
repository.remove()
|
||||
messages.success(request, _('Repository removed. The remote backup '
|
||||
'itself was not deleted.'))
|
||||
return redirect('backups:index')
|
||||
|
||||
|
||||
def umount_location(request, uuid):
|
||||
remote_locations.umount_uuid(uuid)
|
||||
if remote_locations.uuid_is_mounted(uuid):
|
||||
def umount_repository(request, uuid):
|
||||
repository = SshBorgRepository(uuid=uuid)
|
||||
repository.umount()
|
||||
if repository.is_mounted:
|
||||
messages.error(request, _('Unmounting failed!'))
|
||||
return redirect('backups:index')
|
||||
|
||||
|
||||
def mount_location(request, uuid):
|
||||
def mount_repository(request, uuid):
|
||||
repository = SshBorgRepository(uuid=uuid)
|
||||
try:
|
||||
remote_locations.mount_uuid(uuid)
|
||||
repository.mount()
|
||||
except Exception as err:
|
||||
msg = "%s: %s" % (_('Mounting failed'), str(err))
|
||||
messages.error(request, msg)
|
||||
else:
|
||||
if not remote_locations.uuid_is_mounted(uuid):
|
||||
if not repository.is_mounted:
|
||||
messages.error(request, _('Mounting failed'))
|
||||
return redirect('backups:index')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user