nextcloud: Add backup/restore

Signed-off-by: Benedek Nagy <contact@nbenedek.me>
[sunil: Simplify method signature and name for setting maint. mode]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Benedek Nagy 2023-10-05 22:14:31 +02:00 committed by Sunil Mohan Adapa
parent 3d8967a20a
commit 8ef680f450
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
3 changed files with 114 additions and 10 deletions

View File

@ -8,6 +8,7 @@ from plinth import frontpage, menu
from plinth.config import DropinConfigs
from plinth.daemon import Daemon, SharedDaemon
from plinth.modules.apache.components import Webserver, diagnose_url
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import (Firewall,
FirewallLocalProtection)
from plinth.package import Packages
@ -104,6 +105,10 @@ class NextcloudApp(app_module.App):
daemon = SharedDaemon('shared-daemon-nextcloud-mysql', 'mysql')
self.add(daemon)
backup_restore = NextcloudBackupRestore('backup-restore-nextcloud',
**manifest.backup)
self.add(backup_restore)
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
@ -121,3 +126,17 @@ class NextcloudApp(app_module.App):
results = super().diagnose()
results.append(diagnose_url('docker.com'))
return results
class NextcloudBackupRestore(BackupRestore):
"""Component to backup/restore Nextcloud."""
def backup_pre(self, packet):
"""Save database contents."""
super().backup_pre(packet)
privileged.dump_database()
def restore_post(self, packet):
"""Restore database contents."""
super().restore_post(packet)
privileged.restore_database()

View File

@ -46,4 +46,14 @@ clients = [{
}]
}]
backup = {}
backup = {
'data': {
'directories': [
'/var/lib/containers/storage/volumes/nextcloud-volume-fbx/'
],
'files': [
'/var/lib/plinth/backups-data/nextcloud-database.sql',
'/etc/redis/redis.conf'
]
}
}

View File

@ -4,6 +4,7 @@
import os
import pathlib
import random
import re
import string
import subprocess
import time
@ -32,6 +33,9 @@ NEXTCLOUD_CRON_SERVICE_FILE = pathlib.Path(
NEXTCLOUD_CRON_TIMER_FILE = pathlib.Path(
f'{SYSTEMD_LOCATION}nextcloud-cron-fbx.timer')
DB_BACKUP_FILE = pathlib.Path(
'/var/lib/plinth/backups-data/nextcloud-database.sql')
@privileged
def setup():
@ -146,11 +150,22 @@ def _create_database(db_password):
_db_file_path = pathlib.Path('/var/lib/mysql/nextcloud_fbx')
if _db_file_path.exists():
return
query = f'''CREATE USER '{DB_USER}'@'{CONTAINER_IP}'
IDENTIFIED BY'{db_password}';
CREATE DATABASE {DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
GRANT ALL PRIVILEGES ON {DB_NAME}.* TO '{DB_USER}'@'{CONTAINER_IP}';
FLUSH PRIVILEGES;'''
query = f'''CREATE DATABASE {DB_NAME} CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
'''
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
check=True)
_set_db_privileges(db_password)
def _set_db_privileges(db_password):
"""Create user, set password and provide permissions on the database."""
query = f'''GRANT ALL PRIVILEGES ON {DB_NAME}.* TO
'{DB_USER}'@'{CONTAINER_IP}'
IDENTIFIED BY'{db_password}';
FLUSH PRIVILEGES;
'''
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
check=True)
@ -261,10 +276,11 @@ def _remove_db_socket():
def _drop_database():
"""Drop the mysql database that was created during install."""
query = f'''DROP DATABASE {DB_NAME};
DROP User '{DB_USER}'@'{CONTAINER_IP}';'''
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
check=True)
with action_utils.service_ensure_running('mysql'):
query = f'''DROP DATABASE {DB_NAME};
DROP User '{DB_USER}'@'{CONTAINER_IP}';'''
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
check=True)
def _generate_secret_key(length=64, chars=None):
@ -272,3 +288,62 @@ def _generate_secret_key(length=64, chars=None):
chars = chars or (string.ascii_letters + string.digits)
rand = random.SystemRandom()
return ''.join(rand.choice(chars) for _ in range(length))
def _set_maintenance_mode(on: bool):
"""Turn maintenance mode on or off."""
_run_occ('maintenance:mode', '--on' if on else '--off')
@privileged
def dump_database():
"""Dump database to file."""
_set_maintenance_mode(True)
DB_BACKUP_FILE.parent.mkdir(parents=True, exist_ok=True)
with action_utils.service_ensure_running('mysql'):
with DB_BACKUP_FILE.open('w', encoding='utf-8') as file_handle:
subprocess.run([
'mysqldump', '--add-drop-database', '--add-drop-table',
'--add-drop-trigger', '--single-transaction',
'--default-character-set=utf8mb4', '--user', 'root',
'--databases', DB_NAME
], stdout=file_handle, check=True)
_set_maintenance_mode(False)
@privileged
def restore_database():
"""Restore database from file."""
with action_utils.service_ensure_running('mysql'):
with DB_BACKUP_FILE.open('r', encoding='utf-8') as file_handle:
subprocess.run(['mysql', '--user', 'root'], stdin=file_handle,
check=True)
_set_db_privileges(_get_dbpassword())
action_utils.service_restart('redis-server')
_set_maintenance_mode(False)
# Attempts to update UUIDs of user and group entries. By default,
# the command attempts to update UUIDs that have been invalidated by
# a migration step.
_run_occ('ldap:update-uuid')
# Update the systems data-fingerprint after a backup is restored
_run_occ('maintenance:data-fingerprint')
def _get_dbpassword():
"""Return the database password from config.php.
OCC cannot run unless Nextcloud can already connect to the database.
"""
config_file = ('/var/lib/containers/storage/volumes/nextcloud-volume-fbx'
'/_data/config/config.php')
with open(config_file, 'r', encoding='utf-8') as config:
config_contents = config.read()
pattern = r"'{}'\s*=>\s*'([^']*)'".format(re.escape('dbpassword'))
match = re.search(pattern, config_contents)
return match.group(1)