[fix] Syntax and import error

This commit is contained in:
ljf 2022-10-10 02:13:15 +02:00
parent c2783e452a
commit e8c0be1e56
No known key found for this signature in database
5 changed files with 143 additions and 75 deletions

View file

@ -1021,6 +1021,9 @@ backup:
arguments: arguments:
name: name:
help: Name of the local backup archive help: Name of the local backup archive
-r:
full: --repository
help: The archive repository (local borg repo use by default)
--system: --system:
help: List of system parts to restore (or all if none is given) help: List of system parts to restore (or all if none is given)
nargs: "*" nargs: "*"

View file

@ -0,0 +1,89 @@
version = "1.0"
i18n = "repository_config"
[main]
name.en = ""
[main.main]
name.en = ""
optional = false
# if method == "tar": question["value"] = False
[main.main.description]
type = "string"
default = ""
[main.main.is_remote]
type = "boolean"
yes = true
no = false
visible = "creation"
default = "no"
[main.main.domain]
type = "string"
visible = "creation && is_remote"
pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$'
pattern.error = 'domain_error' # TODO "Please provide a valid domain"
default = ""
# FIXME: can't be a domain of this instances ?
[main.main.is_shf]
help = ""
type = "boolean"
yes = true
no = false
visible = "creation && is_remote"
default = false
[main.main.public_key]
type = "alert"
style = "info"
visible = "creation && is_remote && ! is_shf"
[main.main.alert]
help = ''
type = "tags"
visible = "is_remote && is_shf"
pattern.regexp = '^[\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$'
pattern.error = "alert_error"
default = []
# "value": alert,
[main.main.alert_delay]
help = ''
type = "number"
visible = "is_remote && is_shf"
min = 1
default = 7
[main.main.quota]
type = "string"
visible = "is_remote && is_shf"
pattern.regexp = '^\d+[MGT]$'
pattern.error = '' # TODO ""
default = ""
[main.main.port]
type = "number"
visible = "is_remote && !is_shf"
min = 1
max = 65535
default = 22
[main.main.user]
type = "string"
visible = "is_remote && !is_shf"
default = ""
[main.main.method]
type = "select"
# "value": method,
choices.borg = "BorgBackup (recommended)"
choices.tar = "Legacy tar archive mechanism"
default = "borg"
visible = "!is_remote"
[main.main.path]
type = "path"
visible = "!is_remote or (is_remote and !is_shf)"
default = "/home/yunohost.backup/archives"

View file

@ -32,13 +32,12 @@ import csv
import tempfile import tempfile
import re import re
import urllib.parse import urllib.parse
from datetime import datetime
from packaging import version from packaging import version
from moulinette import Moulinette, m18n from moulinette import Moulinette, m18n
from moulinette.utils import filesystem from moulinette.utils import filesystem
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import mkdir, write_to_yaml, read_yaml, write_to_file from moulinette.utils.filesystem import mkdir, write_to_yaml, read_yaml, write_to_file, rm
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
import yunohost.domain import yunohost.domain
@ -64,7 +63,7 @@ from yunohost.tools import (
from yunohost.regenconf import regen_conf from yunohost.regenconf import regen_conf
from yunohost.log import OperationLogger, is_unit_operation from yunohost.log import OperationLogger, is_unit_operation
from yunohost.repository import BackupRepository, BackupArchive from yunohost.repository import BackupRepository, BackupArchive
from yunohost.config import ConfigPanel from yunohost.utils.config import ConfigPanel
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.packages import ynh_packages_version from yunohost.utils.packages import ynh_packages_version
from yunohost.utils.filesystem import free_space_in_directory, disk_usage, binary_to_human from yunohost.utils.filesystem import free_space_in_directory, disk_usage, binary_to_human
@ -74,7 +73,6 @@ ARCHIVES_PATH = f"{BACKUP_PATH}/archives"
APP_MARGIN_SPACE_SIZE = 100 # In MB APP_MARGIN_SPACE_SIZE = 100 # In MB
CONF_MARGIN_SPACE_SIZE = 10 # IN MB CONF_MARGIN_SPACE_SIZE = 10 # IN MB
POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB
MB_ALLOWED_TO_ORGANIZE = 10
logger = getActionLogger("yunohost.backup") logger = getActionLogger("yunohost.backup")
@ -367,6 +365,14 @@ class BackupManager:
filesystem.rm(self.work_dir, recursive=True, force=True) filesystem.rm(self.work_dir, recursive=True, force=True)
filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin")
def clean_work_dir(self, umount=True):
if umount and not _recursive_umount(self.work_dir):
raise YunohostError("backup_cleaning_failed")
if self.is_tmp_work_dir:
rm(self.work_dir, True, True)
# #
# Backup target management # # Backup target management #
# #
@ -875,18 +881,19 @@ class RestoreManager:
return restore_manager.result return restore_manager.result
""" """
def __init__(self, name, method="tar"): def __init__(self, archive):
""" """
RestoreManager constructor RestoreManager constructor
Args: Args:
name -- (string) Archive name archive -- (BackupArchive) The archive to restore
method -- (string) Method name to use to mount the archive
""" """
# Retrieve and open the archive # Retrieve and open the archive
# FIXME this way to get the info is not compatible with copy or custom # FIXME this way to get the info is not compatible with copy or custom
self.archive = archive
# backup methods # backup methods
self.info = backup_info(name, with_details=True) self.info = archive.info() # FIXME with_details=True
from_version = self.info.get("from_yunohost_version", "") from_version = self.info.get("from_yunohost_version", "")
# Remove any '~foobar' in the version ... c.f ~alpha, ~beta version during # Remove any '~foobar' in the version ... c.f ~alpha, ~beta version during
@ -896,9 +903,6 @@ class RestoreManager:
if not from_version or version.parse(from_version) < version.parse("4.2.0"): if not from_version or version.parse(from_version) < version.parse("4.2.0"):
raise YunohostValidationError("restore_backup_too_old") raise YunohostValidationError("restore_backup_too_old")
self.archive_path = self.info["path"]
self.name = name
self.method = BackupMethod.create(method, self)
self.targets = BackupRestoreTargetsManager() self.targets = BackupRestoreTargetsManager()
# #
@ -913,32 +917,6 @@ class RestoreManager:
return len(successful_apps) != 0 or len(successful_system) != 0 return len(successful_apps) != 0 or len(successful_system) != 0
def _read_info_files(self):
"""
Read the info file from inside an archive
"""
# Retrieve backup info
info_file = os.path.join(self.work_dir, "info.json")
try:
with open(info_file, "r") as f:
self.info = json.load(f)
# Historically, "system" was "hooks"
if "system" not in self.info.keys():
self.info["system"] = self.info["hooks"]
except IOError:
logger.debug("unable to load '%s'", info_file, exc_info=1)
raise YunohostError(
"backup_archive_cant_retrieve_info_json", archive=self.archive_path
)
else:
logger.debug(
"restoring from backup '%s' created on %s",
self.name,
datetime.utcfromtimestamp(self.info["created_at"]),
)
def _postinstall_if_needed(self): def _postinstall_if_needed(self):
""" """
Post install yunohost if needed Post install yunohost if needed
@ -1041,10 +1019,8 @@ class RestoreManager:
for hook_path in hook_paths: for hook_path in hook_paths:
logger.debug( logger.debug(
"Adding restoration script '%s' to the system " f"Adding restoration script '{hook_path}' to the system "
"from the backup archive '%s'", f"from the backup archive '{self.archive.archive_path}'"
hook_path,
self.archive_path,
) )
self.method.copy(hook_path, custom_restore_hook_folder) self.method.copy(hook_path, custom_restore_hook_folder)
@ -1096,7 +1072,7 @@ class RestoreManager:
this archive this archive
""" """
self.work_dir = os.path.join(BACKUP_PATH, "tmp", self.name) self.work_dir = os.path.join(BACKUP_PATH, "tmp", self.archive.name)
if os.path.ismount(self.work_dir): if os.path.ismount(self.work_dir):
logger.debug("An already mounting point '%s' already exists", self.work_dir) logger.debug("An already mounting point '%s' already exists", self.work_dir)
@ -1120,9 +1096,7 @@ class RestoreManager:
filesystem.mkdir(self.work_dir, parents=True) filesystem.mkdir(self.work_dir, parents=True)
self.method.extract() self.archive.extract()
self._read_info_files()
# #
# Space computation / checks # # Space computation / checks #
@ -1736,7 +1710,7 @@ def backup_create(
} }
def backup_restore(name, system=[], apps=[], force=False): def backup_restore(name, repository, system=[], apps=[], force=False):
""" """
Restore from a local backup archive Restore from a local backup archive
@ -1757,6 +1731,9 @@ def backup_restore(name, system=[], apps=[], force=False):
system = [] system = []
apps = [] apps = []
if not repository:
repository = "local-borg"
# #
# Initialize # # Initialize #
# #
@ -1766,7 +1743,10 @@ def backup_restore(name, system=[], apps=[], force=False):
elif name.endswith(".tar"): elif name.endswith(".tar"):
name = name[: -len(".tar")] name = name[: -len(".tar")]
restore_manager = RestoreManager(name) repo = BackupRepository(repository)
archive = BackupArchive(name, repo)
restore_manager = RestoreManager(archive)
restore_manager.set_system_targets(system) restore_manager.set_system_targets(system)
restore_manager.set_apps_targets(apps) restore_manager.set_apps_targets(apps)
@ -1827,7 +1807,7 @@ def backup_list(repositories=[], with_info=False, human_readable=False):
""" """
return { return {
name: BackupRepository(name).list(with_info) name: BackupRepository(name).list_archives(with_info)
for name in repositories or BackupRepository.list(full=False) for name in repositories or BackupRepository.list(full=False)
} }
@ -1862,7 +1842,7 @@ def backup_info(name, repository=None, with_details=False, human_readable=False)
repo = BackupRepository(repository) repo = BackupRepository(repository)
archive = BackupArchive(name, repo) archive = BackupArchive(name, repo)
return archive.info() return archive.info(with_details=with_details, human_readable=human_readable)
def backup_delete(name, repository): def backup_delete(name, repository):
@ -1969,7 +1949,7 @@ class BackupTimer(ConfigPanel):
Description=Run backup {self.entity} regularly Description=Run backup {self.entity} regularly
[Timer] [Timer]
OnCalendar={values['schedule']} OnCalendar={self.values['schedule']}
[Install] [Install]
WantedBy=timers.target WantedBy=timers.target
@ -1982,7 +1962,7 @@ After=network.target
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/bin/yunohost backup create -n '{self.entity}' -r '{repo}' --system --apps ; /usr/bin/yunohost backup prune -n '{self.entity}' ExecStart=/usr/bin/yunohost backup create -n '{self.entity}' -r '{self.repositories}' --system --apps ; /usr/bin/yunohost backup prune -n '{self.entity}'
User=root User=root
Group=root Group=root
""") """)

View file

@ -30,6 +30,7 @@ import shutil
import subprocess import subprocess
import tarfile import tarfile
import tempfile import tempfile
from functools import reduce
from moulinette import Moulinette, m18n from moulinette import Moulinette, m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
@ -38,7 +39,7 @@ from moulinette.utils.filesystem import read_file, rm, mkdir
from moulinette.utils.network import download_text from moulinette.utils.network import download_text
from datetime import timedelta, datetime from datetime import timedelta, datetime
import yunohost.repositories
from yunohost.utils.config import ConfigPanel from yunohost.utils.config import ConfigPanel
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.filesystem import disk_usage, binary_to_human, free_space_in_directory from yunohost.utils.filesystem import disk_usage, binary_to_human, free_space_in_directory
@ -48,6 +49,7 @@ logger = getActionLogger('yunohost.repository')
REPOSITORIES_DIR = '/etc/yunohost/repositories' REPOSITORIES_DIR = '/etc/yunohost/repositories'
CACHE_INFO_DIR = "/var/cache/yunohost/{repository}" CACHE_INFO_DIR = "/var/cache/yunohost/{repository}"
REPOSITORY_CONFIG_PATH = "/usr/share/yunohost/other/config_repository.toml" REPOSITORY_CONFIG_PATH = "/usr/share/yunohost/other/config_repository.toml"
MB_ALLOWED_TO_ORGANIZE = 10
# TODO split ConfigPanel.get to extract "Format result" part and be able to override it # TODO split ConfigPanel.get to extract "Format result" part and be able to override it
# TODO Migration # TODO Migration
# TODO Remove BackupRepository.get_or_create() # TODO Remove BackupRepository.get_or_create()
@ -240,7 +242,7 @@ class BackupRepository(ConfigPanel):
return {self.shortname: result} return {self.shortname: result}
def list(self, with_info): def list_archives(self, with_info):
archives = self.list_archive_name() archives = self.list_archive_name()
if with_info: if with_info:
d = {} d = {}
@ -343,14 +345,14 @@ class BackupArchive:
# Cast # Cast
if self.repo.method_name == 'tar': if self.repo.method_name == 'tar':
self.__class__ = TarBackupArchive self.__class__ = yunohost.repositories.tar.TarBackupArchive
elif self.repo.method_name == 'borg': elif self.repo.method_name == 'borg':
self.__class__ = BorgBackupArchive self.__class__ = yunohost.repositories.borg.BorgBackupArchive
else: else:
self.__class__ = HookBackupArchive self.__class__ = yunohost.repositories.hook.HookBackupArchive
# Assert archive exists # Assert archive exists
if not isinstance(self.manager, BackupManager) and self.name not in self.repo.list(): if self.manager.__class__.__name__ != "BackupManager" and self.name not in self.repo.list_archives():
raise YunohostValidationError("backup_archive_name_unknown", name=name) raise YunohostValidationError("backup_archive_name_unknown", name=name)
@property @property
@ -439,17 +441,17 @@ class BackupArchive:
yield f"{leading_dot}apps/{app}" yield f"{leading_dot}apps/{app}"
def _get_info_string(self): def _get_info_string(self):
archive_file = "%s/%s.tar" % (self.repo.path, self.name) self.archive_file = "%s/%s.tar" % (self.repo.path, self.name)
# Check file exist (even if it's a broken symlink) # Check file exist (even if it's a broken symlink)
if not os.path.lexists(archive_file): if not os.path.lexists(self.archive_file):
archive_file += ".gz" self.archive_file += ".gz"
if not os.path.lexists(archive_file): if not os.path.lexists(self.archive_file):
raise YunohostValidationError("backup_archive_name_unknown", name=self.name) raise YunohostValidationError("backup_archive_name_unknown", name=self.name)
# If symlink, retrieve the real path # If symlink, retrieve the real path
if os.path.islink(archive_file): if os.path.islink(self.archive_file):
archive_file = os.path.realpath(archive_file) archive_file = os.path.realpath(self.archive_file)
# Raise exception if link is broken (e.g. on unmounted external storage) # Raise exception if link is broken (e.g. on unmounted external storage)
if not os.path.exists(archive_file): if not os.path.exists(archive_file):
@ -470,7 +472,7 @@ class BackupArchive:
self.extract("./info.json") self.extract("./info.json")
else: else:
raise YunohostError( raise YunohostError(
"backup_archive_cant_retrieve_info_json", archive=archive_file "backup_archive_cant_retrieve_info_json", archive=self.archive_file
) )
shutil.move(os.path.join(info_dir, "info.json"), info_file) shutil.move(os.path.join(info_dir, "info.json"), info_file)
finally: finally:
@ -482,7 +484,7 @@ class BackupArchive:
logger.debug("unable to load '%s'", info_file, exc_info=1) logger.debug("unable to load '%s'", info_file, exc_info=1)
raise YunohostError('backup_invalid_archive') raise YunohostError('backup_invalid_archive')
def info(self): def info(self, with_details, human_readable):
info_json = self._get_info_string() info_json = self._get_info_string()
if not self._info_json: if not self._info_json:
@ -498,14 +500,14 @@ class BackupArchive:
size = info.get("size", 0) size = info.get("size", 0)
if not size: if not size:
tar = tarfile.open( tar = tarfile.open(
archive_file, "r:gz" if archive_file.endswith(".gz") else "r" self.archive_file, "r:gz" if self.archive_file.endswith(".gz") else "r"
) )
size = reduce( size = reduce(
lambda x, y: getattr(x, "size", x) + getattr(y, "size", y), tar.getmembers() lambda x, y: getattr(x, "size", x) + getattr(y, "size", y), tar.getmembers()
) )
tar.close() tar.close()
result = { result = {
"path": repo.archive_path, "path": self.repo.archive_path,
"created_at": datetime.utcfromtimestamp(info["created_at"]), "created_at": datetime.utcfromtimestamp(info["created_at"]),
"description": info["description"], "description": info["description"],
"size": size, "size": size,
@ -546,17 +548,11 @@ class BackupArchive:
return info return info
# TODO move this in BackupManager ?????
def clean(self): def clean(self):
""" """
Umount sub directories of working dirextories and delete it if temporary Umount sub directories of working dirextories and delete it if temporary
""" """
if self.need_organized_files(): self.manager.clean_work_dir(self.need_organized_files())
if not _recursive_umount(self.work_dir):
raise YunohostError("backup_cleaning_failed")
if self.manager.is_tmp_work_dir:
rm(self.work_dir, True, True)
def _organize_files(self): def _organize_files(self):
""" """
@ -573,7 +569,7 @@ class BackupArchive:
for path in self.manager.paths_to_backup: for path in self.manager.paths_to_backup:
src = path["source"] src = path["source"]
if self.manager is RestoreManager: if self.manager.__class__.__name__ != "RestoreManager":
# TODO Support to run this before a restore (and not only before # TODO Support to run this before a restore (and not only before
# backup). To do that RestoreManager.unorganized_work_dir should # backup). To do that RestoreManager.unorganized_work_dir should
# be implemented # be implemented