yunohost/src/tests/test_backuprestore.py

680 lines
21 KiB
Python

import pytest
import os
import shutil
import subprocess
from mock import patch
from .conftest import message, raiseYunohostError, get_test_apps_dir
from moulinette.utils.text import random_ascii
from yunohost.app import app_install, app_remove, app_ssowatconf
from yunohost.app import _is_installed
from yunohost.backup import (
backup_create,
backup_restore,
backup_list,
backup_info,
backup_delete,
_recursive_umount,
)
from yunohost.domain import _get_maindomain, domain_list, domain_add, domain_remove
from yunohost.user import user_create, user_list, user_delete
from yunohost.permission import user_permission_list
from yunohost.tests.test_permission import (
check_LDAP_db_integrity,
check_permission_for_apps,
)
from yunohost.hook import CUSTOM_HOOK_FOLDER
# Get main domain
maindomain = ""
def setup_function(function):
global maindomain
maindomain = _get_maindomain()
assert backup_test_dependencies_are_met()
clean_tmp_backup_directory()
reset_ssowat_conf()
delete_all_backups()
uninstall_test_apps_if_needed()
assert len(backup_list()["archives"]) == 0
markers = {
m.name: {"args": m.args, "kwargs": m.kwargs}
for m in function.__dict__.get("pytestmark", [])
}
if "with_wordpress_archive_from_4p2" in markers:
add_archive_wordpress_from_4p2()
assert len(backup_list()["archives"]) == 1
if "with_legacy_app_installed" in markers:
assert not app_is_installed("legacy_app")
install_app("legacy_app_ynh", "/yolo")
assert app_is_installed("legacy_app")
if "with_backup_recommended_app_installed" in markers:
assert not app_is_installed("backup_recommended_app")
install_app(
"backup_recommended_app_ynh", "/yolo", "&helper_to_test=ynh_restore_file"
)
assert app_is_installed("backup_recommended_app")
if "with_backup_recommended_app_installed_with_ynh_restore" in markers:
assert not app_is_installed("backup_recommended_app")
install_app(
"backup_recommended_app_ynh", "/yolo", "&helper_to_test=ynh_restore"
)
assert app_is_installed("backup_recommended_app")
if "with_system_archive_from_4p2" in markers:
add_archive_system_from_4p2()
assert len(backup_list()["archives"]) == 1
if "with_permission_app_installed" in markers:
assert not app_is_installed("permissions_app")
user_create("alice", maindomain, "test123Ynh", fullname="Alice White")
with patch.object(os, "isatty", return_value=False):
install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice")
assert app_is_installed("permissions_app")
if "with_custom_domain" in markers:
domain = markers["with_custom_domain"]["args"][0]
if domain not in domain_list()["domains"]:
domain_add(domain)
def teardown_function(function):
assert tmp_backup_directory_is_empty()
reset_ssowat_conf()
delete_all_backups()
uninstall_test_apps_if_needed()
markers = {
m.name: {"args": m.args, "kwargs": m.kwargs}
for m in function.__dict__.get("pytestmark", [])
}
if "clean_opt_dir" in markers:
shutil.rmtree("/opt/test_backup_output_directory")
if "alice" in user_list()["users"]:
user_delete("alice")
if "with_custom_domain" in markers:
domain = markers["with_custom_domain"]["args"][0]
if domain != maindomain:
domain_remove(domain)
@pytest.fixture(autouse=True)
def check_LDAP_db_integrity_call():
check_LDAP_db_integrity()
yield
check_LDAP_db_integrity()
@pytest.fixture(autouse=True)
def check_permission_for_apps_call():
check_permission_for_apps()
yield
check_permission_for_apps()
#
# Helpers #
#
def app_is_installed(app):
if app == "permissions_app":
return _is_installed(app)
# These are files we know should be installed by the app
app_files = []
app_files.append("/etc/nginx/conf.d/{}.d/{}.conf".format(maindomain, app))
app_files.append("/var/www/%s/index.html" % app)
app_files.append("/etc/importantfile")
return _is_installed(app) and all(os.path.exists(f) for f in app_files)
def backup_test_dependencies_are_met():
# Dummy test apps (or backup archives)
assert os.path.exists(
os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2")
)
assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh"))
assert os.path.exists(
os.path.join(get_test_apps_dir(), "backup_recommended_app_ynh")
)
return True
def tmp_backup_directory_is_empty():
if not os.path.exists("/home/yunohost.backup/tmp/"):
return True
else:
return len(os.listdir("/home/yunohost.backup/tmp/")) == 0
def clean_tmp_backup_directory():
if tmp_backup_directory_is_empty():
return
mount_lines = subprocess.check_output("mount").decode().split("\n")
points_to_umount = [
line.split(" ")[2]
for line in mount_lines
if len(line) >= 3 and line.split(" ")[2].startswith("/home/yunohost.backup/tmp")
]
for point in reversed(points_to_umount):
os.system("umount %s" % point)
for f in os.listdir("/home/yunohost.backup/tmp/"):
shutil.rmtree("/home/yunohost.backup/tmp/%s" % f)
shutil.rmtree("/home/yunohost.backup/tmp/")
def reset_ssowat_conf():
# Make sure we have a ssowat
os.system("mkdir -p /etc/ssowat/")
app_ssowatconf()
def delete_all_backups():
for archive in backup_list()["archives"]:
backup_delete(archive)
def uninstall_test_apps_if_needed():
for app in ["legacy_app", "backup_recommended_app", "wordpress", "permissions_app"]:
if _is_installed(app):
app_remove(app)
def install_app(app, path, additionnal_args=""):
app_install(
os.path.join(get_test_apps_dir(), app),
args="domain={}&path={}{}".format(maindomain, path, additionnal_args),
force=True,
)
def add_archive_wordpress_from_4p2():
os.system("mkdir -p /home/yunohost.backup/archives")
os.system(
"cp "
+ os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2/backup.tar")
+ " /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar"
)
def add_archive_system_from_4p2():
os.system("mkdir -p /home/yunohost.backup/archives")
os.system(
"cp "
+ os.path.join(get_test_apps_dir(), "backup_system_from_4p2/backup.tar")
+ " /home/yunohost.backup/archives/backup_system_from_4p2.tar"
)
#
# System backup #
#
def test_backup_only_ldap(mocker):
# Create the backup
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
backup_create(name=name, system=["conf_ldap"], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
archives_info = backup_info(archives[0], with_details=True)
assert archives_info["apps"] == {}
assert len(archives_info["system"].keys()) == 1
assert "conf_ldap" in archives_info["system"].keys()
def test_backup_system_part_that_does_not_exists(mocker):
# Create the backup
with message(mocker, "backup_hook_unknown", hook="doesnt_exist"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=["doesnt_exist"], apps=None)
#
# System backup and restore #
#
def test_backup_and_restore_all_sys(mocker):
name = random_ascii(8)
# Create the backup
with message(mocker, "backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
archives_info = backup_info(archives[0], with_details=True)
assert archives_info["apps"] == {}
assert len(archives_info["system"].keys()) == len(
os.listdir("/usr/share/yunohost/hooks/backup/")
)
# Remove ssowat conf
assert os.path.exists("/etc/ssowat/conf.json")
os.system("rm -rf /etc/ssowat/")
assert not os.path.exists("/etc/ssowat/conf.json")
# Restore the backup
with message(mocker, "restore_complete"):
backup_restore(name=archives[0], force=True, system=[], apps=None)
# Check ssowat conf is back
assert os.path.exists("/etc/ssowat/conf.json")
#
# System restore from 3.8 #
#
@pytest.mark.with_system_archive_from_4p2
def test_restore_system_from_Ynh4p2(monkeypatch, mocker):
name = random_ascii(8)
# Backup current system
with message(mocker, "backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 2
# Restore system archive from 3.8
try:
with message(mocker, "restore_complete"):
backup_restore(
name=backup_list()["archives"][1], system=[], apps=None, force=True
)
finally:
# Restore system as it was
backup_restore(
name=backup_list()["archives"][0], system=[], apps=None, force=True
)
#
# App backup #
#
@pytest.mark.with_backup_recommended_app_installed
def test_backup_script_failure_handling(monkeypatch, mocker):
def custom_hook_exec(name, *args, **kwargs):
if os.path.basename(name).startswith("backup_"):
raise Exception
else:
return True
# Create a backup of this app and simulate a crash (patching the backup
# call with monkeypatch). We also patch m18n to check later it's been called
# with the expected error message key
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
with message(mocker, "backup_app_failed", app="backup_recommended_app"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.with_backup_recommended_app_installed
def test_backup_not_enough_free_space(monkeypatch, mocker):
def custom_space_used_by_directory(path, *args, **kwargs):
return 99999999999999999
def custom_free_space_in_directory(dirpath):
return 0
monkeypatch.setattr(
"yunohost.backup.space_used_by_directory", custom_space_used_by_directory
)
monkeypatch.setattr(
"yunohost.backup.free_space_in_directory", custom_free_space_in_directory
)
with raiseYunohostError(mocker, "not_enough_disk_space"):
backup_create(system=None, apps=["backup_recommended_app"])
def test_backup_app_not_installed(mocker):
assert not _is_installed("wordpress")
with message(mocker, "unbackup_app", app="wordpress"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["wordpress"])
@pytest.mark.with_backup_recommended_app_installed
def test_backup_app_with_no_backup_script(mocker):
backup_script = "/etc/yunohost/apps/backup_recommended_app/scripts/backup"
os.system("rm %s" % backup_script)
assert not os.path.exists(backup_script)
with message(
mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"
):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.with_backup_recommended_app_installed
def test_backup_app_with_no_restore_script(mocker):
restore_script = "/etc/yunohost/apps/backup_recommended_app/scripts/restore"
os.system("rm %s" % restore_script)
assert not os.path.exists(restore_script)
# Backuping an app with no restore script will only display a warning to the
# user...
with message(
mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"
):
backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.clean_opt_dir
def test_backup_with_different_output_directory(mocker):
name = random_ascii(8)
# Create the backup
with message(mocker, "backup_created", name=name):
backup_create(
system=["conf_ynh_settings"],
apps=None,
output_directory="/opt/test_backup_output_directory",
name=name,
)
assert os.path.exists(f"/opt/test_backup_output_directory/{name}.tar")
archives = backup_list()["archives"]
assert len(archives) == 1
archives_info = backup_info(archives[0], with_details=True)
assert archives_info["apps"] == {}
assert len(archives_info["system"].keys()) == 1
assert "conf_ynh_settings" in archives_info["system"].keys()
@pytest.mark.clean_opt_dir
def test_backup_using_copy_method(mocker):
# Create the backup
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
backup_create(
system=["conf_ynh_settings"],
apps=None,
output_directory="/opt/test_backup_output_directory",
methods=["copy"],
name=name,
)
assert os.path.exists("/opt/test_backup_output_directory/info.json")
#
# App restore #
#
@pytest.mark.with_wordpress_archive_from_4p2
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_wordpress_from_Ynh4p2(mocker):
with message(mocker, "restore_complete"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
@pytest.mark.with_wordpress_archive_from_4p2
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_script_failure_handling(monkeypatch, mocker):
def custom_hook_exec(name, *args, **kwargs):
if os.path.basename(name).startswith("restore"):
monkeypatch.undo()
return (1, None)
monkeypatch.setattr("yunohost.hook.hook_exec", custom_hook_exec)
assert not _is_installed("wordpress")
with message(mocker, "app_restore_script_failed"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert not _is_installed("wordpress")
@pytest.mark.with_wordpress_archive_from_4p2
def test_restore_app_not_enough_free_space(monkeypatch, mocker):
def custom_free_space_in_directory(dirpath):
return 0
monkeypatch.setattr(
"yunohost.backup.free_space_in_directory", custom_free_space_in_directory
)
assert not _is_installed("wordpress")
with raiseYunohostError(mocker, "restore_not_enough_disk_space"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert not _is_installed("wordpress")
@pytest.mark.with_wordpress_archive_from_4p2
def test_restore_app_not_in_backup(mocker):
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
with message(mocker, "backup_archive_app_not_found", app="yoloswag"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["yoloswag"]
)
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
@pytest.mark.with_wordpress_archive_from_4p2
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_already_installed(mocker):
assert not _is_installed("wordpress")
with message(mocker, "restore_complete"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert _is_installed("wordpress")
with raiseYunohostError(mocker, "restore_already_installed_apps"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert _is_installed("wordpress")
@pytest.mark.with_legacy_app_installed
def test_backup_and_restore_legacy_app(mocker):
_test_backup_and_restore_app(mocker, "legacy_app")
@pytest.mark.with_backup_recommended_app_installed
def test_backup_and_restore_recommended_app(mocker):
_test_backup_and_restore_app(mocker, "backup_recommended_app")
@pytest.mark.with_backup_recommended_app_installed_with_ynh_restore
def test_backup_and_restore_with_ynh_restore(mocker):
_test_backup_and_restore_app(mocker, "backup_recommended_app")
@pytest.mark.with_permission_app_installed
def test_backup_and_restore_permission_app(mocker):
res = user_permission_list(full=True)["permissions"]
assert "permissions_app.main" in res
assert "permissions_app.admin" in res
assert "permissions_app.dev" in res
assert res["permissions_app.main"]["url"] == "/"
assert res["permissions_app.admin"]["url"] == "/admin"
assert res["permissions_app.dev"]["url"] == "/dev"
assert "visitors" in res["permissions_app.main"]["allowed"]
assert "all_users" in res["permissions_app.main"]["allowed"]
assert res["permissions_app.admin"]["allowed"] == ["alice"]
assert res["permissions_app.dev"]["allowed"] == []
_test_backup_and_restore_app(mocker, "permissions_app")
res = user_permission_list(full=True)["permissions"]
assert "permissions_app.main" in res
assert "permissions_app.admin" in res
assert "permissions_app.dev" in res
assert res["permissions_app.main"]["url"] == "/"
assert res["permissions_app.admin"]["url"] == "/admin"
assert res["permissions_app.dev"]["url"] == "/dev"
assert "visitors" in res["permissions_app.main"]["allowed"]
assert "all_users" in res["permissions_app.main"]["allowed"]
assert res["permissions_app.admin"]["allowed"] == ["alice"]
assert res["permissions_app.dev"]["allowed"] == []
def _test_backup_and_restore_app(mocker, app):
# Create a backup of this app
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
backup_create(name=name, system=None, apps=[app])
archives = backup_list()["archives"]
assert len(archives) == 1
archives_info = backup_info(archives[0], with_details=True)
assert archives_info["system"] == {}
assert len(archives_info["apps"].keys()) == 1
assert app in archives_info["apps"].keys()
# Uninstall the app
app_remove(app)
assert not app_is_installed(app)
assert app + ".main" not in user_permission_list()["permissions"]
# Restore the app
with message(mocker, "restore_complete"):
backup_restore(system=None, name=archives[0], apps=[app])
assert app_is_installed(app)
# Check permission
per_list = user_permission_list()["permissions"]
assert app + ".main" in per_list
#
# Some edge cases #
#
def test_restore_archive_with_no_json(mocker):
# Create a backup with no info.json associated
os.system("touch /tmp/afile")
os.system("tar -cvf /home/yunohost.backup/archives/badbackup.tar /tmp/afile")
assert "badbackup" in backup_list()["archives"]
with raiseYunohostError(mocker, "backup_archive_cant_retrieve_info_json"):
backup_restore(name="badbackup", force=True)
@pytest.mark.with_wordpress_archive_from_4p2
def test_restore_archive_with_bad_archive(mocker):
# Break the archive
os.system(
"head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_4p2_bad.tar"
)
assert "backup_wordpress_from_4p2_bad" in backup_list()["archives"]
with raiseYunohostError(mocker, "backup_archive_corrupted"):
backup_restore(name="backup_wordpress_from_4p2_bad", force=True)
clean_tmp_backup_directory()
def test_restore_archive_with_custom_hook(mocker):
custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore")
os.system("touch %s/99-yolo" % custom_restore_hook_folder)
# Backup with custom hook system
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
# Restore system with custom hook
with message(mocker, "restore_complete"):
backup_restore(
name=backup_list()["archives"][0], system=[], apps=None, force=True
)
os.system("rm %s/99-yolo" % custom_restore_hook_folder)
def test_backup_binds_are_readonly(mocker, monkeypatch):
def custom_mount_and_backup(self):
self._organize_files()
conf = os.path.join(self.work_dir, "conf/ynh/dkim")
output = subprocess.check_output(
"touch %s/test 2>&1 || true" % conf,
shell=True,
env={"LANG": "en_US.UTF-8"},
)
output = output.decode()
assert "Read-only file system" in output
if not _recursive_umount(self.work_dir):
raise Exception("Backup cleaning failed !")
self.clean()
monkeypatch.setattr(
"yunohost.backup.BackupMethod.mount_and_backup", custom_mount_and_backup
)
# Create the backup
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
backup_create(name=name, system=[])