mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[wip] Rework backup repository with config panel
This commit is contained in:
parent
52ba727819
commit
f0bff8f021
4 changed files with 230 additions and 114 deletions
|
@ -1161,33 +1161,43 @@ backup:
|
||||||
### backup_repository_add()
|
### backup_repository_add()
|
||||||
add:
|
add:
|
||||||
action_help: Add a backup repository
|
action_help: Add a backup repository
|
||||||
api: POST /backup/repository/<name>
|
api: POST /backup/repository/<shortname>
|
||||||
arguments:
|
arguments:
|
||||||
location:
|
shortname:
|
||||||
|
help: ID of the repository
|
||||||
|
extra:
|
||||||
|
pattern: &pattern_backup_repository_id
|
||||||
|
- !!str ^\w+$
|
||||||
|
- "pattern_backup_repository_id"
|
||||||
|
-n:
|
||||||
|
full: --name
|
||||||
|
help: Short description of the repository
|
||||||
|
-l:
|
||||||
|
full: --location
|
||||||
help: Location on this server or on an other
|
help: Location on this server or on an other
|
||||||
extra:
|
extra:
|
||||||
pattern: &pattern_backup_repository_location
|
pattern: &pattern_backup_repository_location
|
||||||
- !!str ^((ssh://)?[a-z_]\w*@\[\w\-\.]+:)?(~?/)?[\w/]*$
|
- !!str ^((ssh://)?[a-z_]\w*@\[\w\-\.]+:)?(~?/)?[\w/]*$
|
||||||
- "pattern_backup_repository_location"
|
- "pattern_backup_repository_location"
|
||||||
-n:
|
-m:
|
||||||
full: --name
|
full: --method
|
||||||
help: Name of the repository
|
help: By default 'borg' method is used, could be 'tar' or a custom method
|
||||||
extra:
|
default: borg
|
||||||
pattern: &pattern_backup_repository_name
|
|
||||||
- !!str ^\w+$
|
|
||||||
- "pattern_backup_repository_name"
|
|
||||||
-d:
|
|
||||||
full: --description
|
|
||||||
help: Short description of the repository
|
|
||||||
--methods:
|
|
||||||
help: List of backup methods accepted
|
|
||||||
nargs: "*"
|
|
||||||
-q:
|
-q:
|
||||||
full: --quota
|
full: --quota
|
||||||
help: Quota to configure with this repository
|
help: Quota to configure with this repository
|
||||||
-e:
|
-p:
|
||||||
full: --encryption
|
full: --passphrase
|
||||||
help: Type of encryption
|
help: A strong passphrase to encrypt/decrypt your backup (keep it preciously)
|
||||||
|
action: store_true
|
||||||
|
-a:
|
||||||
|
full: --alert
|
||||||
|
help: List of mails to which sent inactivity alert
|
||||||
|
nargs: "*"
|
||||||
|
-d:
|
||||||
|
full: --alert-delay
|
||||||
|
help: Inactivity delay in days after which we sent alerts mails
|
||||||
|
default: 7
|
||||||
|
|
||||||
### backup_repository_update()
|
### backup_repository_update()
|
||||||
update:
|
update:
|
||||||
|
|
83
data/other/config_repository.toml
Normal file
83
data/other/config_repository.toml
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
i18n = "repository_config"
|
||||||
|
[main]
|
||||||
|
name.en = ""
|
||||||
|
[]
|
||||||
|
name.en = ""
|
||||||
|
# if method == "tar": question["value"] = False
|
||||||
|
[creation] # TODO "Remote repository"
|
||||||
|
type = "boolean"
|
||||||
|
visible = "false"
|
||||||
|
|
||||||
|
[name] # TODO "Remote repository"
|
||||||
|
type = "string"
|
||||||
|
|
||||||
|
[is_remote] # TODO "Remote repository"
|
||||||
|
type = "boolean"
|
||||||
|
yes = true
|
||||||
|
no = false
|
||||||
|
visible = "creation && is_remote"
|
||||||
|
|
||||||
|
[is_f2f] # TODO "It's a YunoHost",
|
||||||
|
help = "" # "Answer yes if the remote server is a YunoHost instance or an other F2F compatible provider",
|
||||||
|
type = "boolean"
|
||||||
|
yes = true
|
||||||
|
no = false
|
||||||
|
visible = "creation && is_remote"
|
||||||
|
|
||||||
|
[public_key] # TODO "Here is the public key to give to your BorgBackup provider : {public_key}"
|
||||||
|
type = "alert"
|
||||||
|
style = "info"
|
||||||
|
visible = "creation && is_remote && ! is_f2f"
|
||||||
|
|
||||||
|
[location]
|
||||||
|
ask = "Remote server domain"
|
||||||
|
type = "string"
|
||||||
|
visible = "creation && is_remote"
|
||||||
|
pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$'
|
||||||
|
pattern.error = '' # TODO "Please provide a valid domain"
|
||||||
|
# FIXME: can't be a domain of this instances ?
|
||||||
|
|
||||||
|
[alert] # TODO "Alert emails"
|
||||||
|
help = '' # TODO Declare emails to which sent inactivity alerts",
|
||||||
|
type = "tags"
|
||||||
|
visible = "is_remote && is_f2f"
|
||||||
|
pattern.regexp = '^[\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$'
|
||||||
|
pattern.error = "It seems it's not a valid email"
|
||||||
|
# "value": alert,
|
||||||
|
|
||||||
|
[alert_delay] # TODO "Alert delay"
|
||||||
|
help = '' # TODO "After how many inactivity days send email alerts",
|
||||||
|
type = "number"
|
||||||
|
visible = "is_remote && is_f2f"
|
||||||
|
min = 1
|
||||||
|
|
||||||
|
[quota] # TODO "Quota"
|
||||||
|
type = "string"
|
||||||
|
visible = "is_remote && is_f2f"
|
||||||
|
pattern.regexp = '^\d+[MGT]$'
|
||||||
|
pattern.error = '' # TODO ""
|
||||||
|
|
||||||
|
[port] # TODO "Port"
|
||||||
|
type = "number"
|
||||||
|
visible = "is_remote && !is_f2f"
|
||||||
|
min = 1
|
||||||
|
max = 65535
|
||||||
|
|
||||||
|
[user] # TODO User
|
||||||
|
type = "string"
|
||||||
|
visible = "is_remote && !is_f2f"
|
||||||
|
|
||||||
|
[method] # TODO "Backup method"
|
||||||
|
type = "select"
|
||||||
|
# "value": method,
|
||||||
|
choices.borg = "BorgBackup (recommended)"
|
||||||
|
choices.tar = "Legacy tar archive mechanism"
|
||||||
|
default = "borg"
|
||||||
|
visible = "!is_remote"
|
||||||
|
|
||||||
|
[path] # TODO "Archive path"
|
||||||
|
type = "path"
|
||||||
|
visible = "!is_remote or (is_remote and !is_f2f)"
|
||||||
|
|
|
@ -1647,7 +1647,7 @@ class BackupMethod(object):
|
||||||
"""
|
"""
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
if not repo or isinstance(repo, basestring):
|
if not repo or isinstance(repo, basestring):
|
||||||
repo = BackupRepository.get_or_create(ARCHIVES_PATH)
|
repo = BackupRepository.get(ARCHIVES_PATH)
|
||||||
self.repo = repo
|
self.repo = repo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2635,7 +2635,7 @@ def backup_list(repos=[], with_info=False, human_readable=False):
|
||||||
result = OrderedDict()
|
result = OrderedDict()
|
||||||
|
|
||||||
if repos == []:
|
if repos == []:
|
||||||
repos = BackupRepository.all()
|
repos = backup_repository_list(full=True)
|
||||||
else:
|
else:
|
||||||
for k, repo in repos:
|
for k, repo in repos:
|
||||||
repos[k] = BackupRepository.get(repo)
|
repos[k] = BackupRepository.get(repo)
|
||||||
|
|
|
@ -27,6 +27,8 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from moulinette import msignals, m18n
|
from moulinette import msignals, m18n
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import MoulinetteError
|
||||||
|
@ -40,58 +42,43 @@ from yunohost.monitor import binary_to_human
|
||||||
from yunohost.log import OperationLogger, is_unit_operation
|
from yunohost.log import OperationLogger, is_unit_operation
|
||||||
|
|
||||||
logger = getActionLogger('yunohost.repository')
|
logger = getActionLogger('yunohost.repository')
|
||||||
REPOSITORIES_PATH = '/etc/yunohost/repositories.yml'
|
REPOSITORIES_PATH = '/etc/yunohost/repositories'
|
||||||
|
REPOSITORY_CONFIG_PATH = "/usr/share/yunohost/other/config_repository.toml"
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# TODO i18n
|
||||||
|
# TODO visible in cli
|
||||||
|
# TODO split COnfigPanel.get to extract "Format result" part and be able to override it
|
||||||
|
# TODO Migration
|
||||||
|
# TODO Remove BackupRepository.get_or_create()
|
||||||
|
# TODO Backup method
|
||||||
|
# TODO auto test F2F by testing .well-known url
|
||||||
|
# TODO API params to get description of forms
|
||||||
|
# TODO tests
|
||||||
|
# TODO detect external hard drive already mounted and suggest it
|
||||||
|
# TODO F2F client detection / add / update / delete
|
||||||
|
# TODO F2F server
|
||||||
|
|
||||||
class BackupRepository(object):
|
class BackupRepository(ConfigPanel):
|
||||||
"""
|
"""
|
||||||
BackupRepository manage all repository the admin added to the instance
|
BackupRepository manage all repository the admin added to the instance
|
||||||
"""
|
"""
|
||||||
repositories = {}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, location, name, *args, **kwargs):
|
def get(cls, shortname):
|
||||||
cls.load()
|
# FIXME
|
||||||
|
|
||||||
return BackupRepository(True, location, name, *args, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, name):
|
|
||||||
cls.load()
|
|
||||||
|
|
||||||
if name not in cls.repositories:
|
if name not in cls.repositories:
|
||||||
raise YunohostError('backup_repository_doesnt_exists', name=name)
|
raise YunohostError('backup_repository_doesnt_exists', name=name)
|
||||||
|
|
||||||
return BackupRepository(False, **cls.repositories[name])
|
return cls.repositories[name]
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, repository):
|
||||||
def load(cls):
|
self.repository = repository
|
||||||
"""
|
self.save_mode = "full"
|
||||||
Read repositories configuration from file
|
super().__init__(
|
||||||
"""
|
config_path=REPOSITORY_CONFIG_PATH,
|
||||||
if cls.repositories != {}:
|
save_path=f"{REPOSITORY_SETTINGS_DIR}/{repository}.yml",
|
||||||
return cls.repositories
|
)
|
||||||
|
|
||||||
if os.path.exists(REPOSITORIES_PATH):
|
|
||||||
try:
|
|
||||||
cls.repositories = read_yaml(REPOSITORIES_PATH)['repositories']
|
|
||||||
except MoulinetteError as e:
|
|
||||||
raise YunohostError(
|
|
||||||
'backup_cant_open_repositories_file', reason=e)
|
|
||||||
return cls.repositories
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def save(cls):
|
|
||||||
"""
|
|
||||||
Save managed repositories to file
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
write_to_json(REPOSITORIES_PATH, cls.repositories)
|
|
||||||
except Exception as e:
|
|
||||||
raise YunohostError('backup_cant_save_repositories_file', reason=e)
|
|
||||||
|
|
||||||
def __init__(self, created, location, name=None, description=None, method=None,
|
|
||||||
encryption=None, quota=None):
|
|
||||||
|
|
||||||
from yunohost.backup import BackupMethod
|
from yunohost.backup import BackupMethod
|
||||||
self.location = location
|
self.location = location
|
||||||
|
@ -102,12 +89,6 @@ class BackupRepository(object):
|
||||||
raise YunohostError(
|
raise YunohostError(
|
||||||
'backup_repository_already_exists', repositories=self.name)
|
'backup_repository_already_exists', repositories=self.name)
|
||||||
|
|
||||||
self.description = description
|
|
||||||
self.encryption = encryption
|
|
||||||
self.quota = quota
|
|
||||||
|
|
||||||
if method is None:
|
|
||||||
method = 'tar' if self.domain is None else 'borg'
|
|
||||||
self.method = BackupMethod.get(method, self)
|
self.method = BackupMethod.get(method, self)
|
||||||
|
|
||||||
def list(self, with_info=False):
|
def list(self, with_info=False):
|
||||||
|
@ -122,21 +103,16 @@ class BackupRepository(object):
|
||||||
return self.used
|
return self.used
|
||||||
|
|
||||||
def purge(self):
|
def purge(self):
|
||||||
|
# TODO F2F delete
|
||||||
self.method.purge()
|
self.method.purge()
|
||||||
|
|
||||||
def delete(self, purge=False):
|
def delete(self, purge=False):
|
||||||
repositories = BackupRepository.repositories
|
|
||||||
|
|
||||||
repositories.pop(self.name)
|
|
||||||
|
|
||||||
BackupRepository.save()
|
|
||||||
|
|
||||||
if purge:
|
if purge:
|
||||||
self.purge()
|
self.purge()
|
||||||
|
|
||||||
def save(self):
|
os.system("rm -rf {REPOSITORY_SETTINGS_DIR}/{self.repository}.yml")
|
||||||
BackupRepository.reposirories[self.name] = self.__dict__
|
|
||||||
BackupRepository.save()
|
|
||||||
|
|
||||||
def _split_location(self):
|
def _split_location(self):
|
||||||
"""
|
"""
|
||||||
|
@ -159,27 +135,41 @@ def backup_repository_list(full=False):
|
||||||
"""
|
"""
|
||||||
List available repositories where put archives
|
List available repositories where put archives
|
||||||
"""
|
"""
|
||||||
repositories = BackupRepository.load()
|
|
||||||
|
|
||||||
if full:
|
try:
|
||||||
|
repositories = [f.rstrip(".yml")
|
||||||
|
for f in os.listdir(REPOSITORIES_PATH)
|
||||||
|
if os.path.isfile(f) and f.endswith(".yml")]
|
||||||
|
except FileNotFoundError:
|
||||||
|
repositories = []
|
||||||
|
|
||||||
|
if not full:
|
||||||
|
return repositories
|
||||||
|
|
||||||
|
# FIXME: what if one repo.yml is corrupted ?
|
||||||
|
repositories = {repo: BackupRepository(repo).get(mode="export")
|
||||||
|
for repo in repositories}
|
||||||
|
|
||||||
return repositories
|
return repositories
|
||||||
else:
|
|
||||||
return repositories.keys()
|
|
||||||
|
|
||||||
|
|
||||||
def backup_repository_info(name, human_readable=True, space_used=False):
|
def backup_repository_info(shortname, human_readable=True, space_used=False):
|
||||||
"""
|
"""
|
||||||
Show info about a repository
|
Show info about a repository
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
name -- Name of the backup repository
|
name -- Name of the backup repository
|
||||||
"""
|
"""
|
||||||
repository = BackupRepository.get(name)
|
Question.operation_logger = operation_logger
|
||||||
|
repository = BackupRepository(shortname)
|
||||||
|
# TODO
|
||||||
if space_used:
|
if space_used:
|
||||||
repository.compute_space_used()
|
repository.compute_space_used()
|
||||||
|
|
||||||
repository = repository.__dict__
|
repository = repository.get(
|
||||||
|
mode="export"
|
||||||
|
)
|
||||||
|
|
||||||
if human_readable:
|
if human_readable:
|
||||||
if 'quota' in repository:
|
if 'quota' in repository:
|
||||||
repository['quota'] = binary_to_human(repository['quota'])
|
repository['quota'] = binary_to_human(repository['quota'])
|
||||||
|
@ -190,58 +180,92 @@ def backup_repository_info(name, human_readable=True, space_used=False):
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation()
|
@is_unit_operation()
|
||||||
def backup_repository_add(operation_logger, location, name, description=None,
|
def backup_repository_add(operation_logger, shortname, name=None, location=None,
|
||||||
methods=None, quota=None, encryption="passphrase"):
|
method=None, quota=None, passphrase=None,
|
||||||
|
alert=[], alert_delay=7):
|
||||||
"""
|
"""
|
||||||
Add a backup repository
|
Add a backup repository
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
location -- Location of the repository (could be a remote location)
|
location -- Location of the repository (could be a remote location)
|
||||||
name -- Name of the backup repository
|
shortname -- Name of the backup repository
|
||||||
description -- An optionnal description
|
name -- An optionnal description
|
||||||
quota -- Maximum size quota of the repository
|
quota -- Maximum size quota of the repository
|
||||||
encryption -- If available, the kind of encryption to use
|
encryption -- If available, the kind of encryption to use
|
||||||
"""
|
"""
|
||||||
repository = BackupRepository(
|
# FIXME i18n
|
||||||
location, name, description, methods, quota, encryption)
|
# Deduce some value from location
|
||||||
|
args = {}
|
||||||
|
args['name'] = name
|
||||||
|
args['creation'] = True
|
||||||
|
if location:
|
||||||
|
args["location"] = location
|
||||||
|
args["is_remote"] = True
|
||||||
|
args["method"] = method if method else "borg"
|
||||||
|
domain_re = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$'
|
||||||
|
if re.match(domain_re, location):
|
||||||
|
args["is_f2f"] = True
|
||||||
|
elif location[0] != "/":
|
||||||
|
args["is_f2f"] = False
|
||||||
|
else:
|
||||||
|
args["is_remote"] = False
|
||||||
|
args["method"] = method
|
||||||
|
elif method == "tar":
|
||||||
|
args["is_remote"] = False
|
||||||
|
if not location:
|
||||||
|
args["method"] = method
|
||||||
|
|
||||||
try:
|
args["quota"] = quota
|
||||||
repository.save()
|
args["passphrase"] = passphrase
|
||||||
except MoulinetteError:
|
args["alert"]= ",".join(alert) if alert else None
|
||||||
raise YunohostError('backup_repository_add_failed',
|
args["alert_delay"]= alert_delay
|
||||||
repository=name, location=location)
|
|
||||||
|
|
||||||
logger.success(m18n.n('backup_repository_added',
|
# TODO validation
|
||||||
repository=name, location=location))
|
# TODO activate service in apply (F2F or not)
|
||||||
|
Question.operation_logger = operation_logger
|
||||||
|
repository = BackupRepository(shortname)
|
||||||
|
return repository.set(
|
||||||
|
args=urllib.parse.urlencode(args),
|
||||||
|
operation_logger=operation_logger
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation()
|
@is_unit_operation()
|
||||||
def backup_repository_update(operation_logger, name, description=None,
|
def backup_repository_update(operation_logger, shortname, name=None,
|
||||||
quota=None, password=None):
|
quota=None, passphrase=None,
|
||||||
|
alert=[], alert_delay=None):
|
||||||
"""
|
"""
|
||||||
Update a backup repository
|
Update a backup repository
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
name -- Name of the backup repository
|
name -- Name of the backup repository
|
||||||
"""
|
"""
|
||||||
repository = BackupRepository.get(name)
|
|
||||||
|
|
||||||
if description is not None:
|
args = {}
|
||||||
repository.description = description
|
args['creation'] = False
|
||||||
|
if name:
|
||||||
|
args['name'] = name
|
||||||
|
if quota:
|
||||||
|
args["quota"] = quota
|
||||||
|
if passphrase:
|
||||||
|
args["passphrase"] = passphrase
|
||||||
|
if alert is not None:
|
||||||
|
args["alert"]= ",".join(alert) if alert else None
|
||||||
|
if alert_delay:
|
||||||
|
args["alert_delay"]= alert_delay
|
||||||
|
|
||||||
if quota is not None:
|
# TODO validation
|
||||||
repository.quota = quota
|
# TODO activate service in apply
|
||||||
|
Question.operation_logger = operation_logger
|
||||||
try:
|
repository = BackupRepository(shortname)
|
||||||
repository.save()
|
return repository.set(
|
||||||
except MoulinetteError:
|
args=urllib.parse.urlencode(args),
|
||||||
raise YunohostError('backup_repository_update_failed', repository=name)
|
operation_logger=operation_logger
|
||||||
logger.success(m18n.n('backup_repository_updated', repository=name,
|
)
|
||||||
location=repository['location']))
|
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation()
|
@is_unit_operation()
|
||||||
def backup_repository_remove(operation_logger, name, purge=False):
|
def backup_repository_remove(operation_logger, shortname, purge=False):
|
||||||
"""
|
"""
|
||||||
Remove a backup repository
|
Remove a backup repository
|
||||||
|
|
||||||
|
@ -249,7 +273,6 @@ def backup_repository_remove(operation_logger, name, purge=False):
|
||||||
name -- Name of the backup repository to remove
|
name -- Name of the backup repository to remove
|
||||||
|
|
||||||
"""
|
"""
|
||||||
repository = BackupRepository.get(name)
|
BackupRepository(shortname).delete(purge)
|
||||||
repository.delete(purge)
|
logger.success(m18n.n('backup_repository_removed', repository=shortname,
|
||||||
logger.success(m18n.n('backup_repository_removed', repository=name,
|
|
||||||
path=repository['path']))
|
path=repository['path']))
|
||||||
|
|
Loading…
Add table
Reference in a new issue