Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ansible/roles/elan.opencast_mariadb
31 changes: 28 additions & 3 deletions .yamllint
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
---
# Yamllint configuration should be compatible with Ansible,
# see https://ansible.readthedocs.io/projects/lint/rules/yaml/#yamllint-configuration

extends: default

rules:
line-length: disable
comments:
# https://github.com/prettier/prettier/issues/6780
min-spaces-from-content: 1
# https://github.com/adrienverge/yamllint/issues/384
comments-indentation: false
document-start: disable
# 160 chars was the default used by old E204 rule, but
# you can easily change it or disable in your .yamllint file.
line-length:
max: 200
# We are adding an extra space inside braces as that's how prettier does it
# and we are trying not to fight other linters.
braces:
min-spaces-inside: 0 # yamllint defaults to 0
max-spaces-inside: 1 # yamllint defaults to 0
# key-duplicates:
# forbid-duplicated-merge-keys: true # not enabled by default
octal-values:
forbid-implicit-octal: true # yamllint defaults to false
forbid-explicit-octal: true # yamllint defaults to false
# quoted-strings:
# quote-type: double
# required: only-when-needed


ignore: |
venv/
.roles/
.cache/
.github/
venv/
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,35 @@ If you want to inspect a running test instance use `molecule login --host <insta

To test the role run `molecule test`.

## Database Backups (optional)

This role can (optionally) install a small backup setup using:
- a backup script placed in `database_backup_output_path`
- a `database-backup.service` + `database-backup.timer` (systemd)

When enabled, backups are written as compressed dumps:
`db-backup-<db>-<timestamp>.sql.gz`

### Backup variables

- `database_backup_enabled` (default: `false`) - Enable/disable the backup feature.
- `database_backup_output_path` (default: empty) - **Required when** `database_backup_enabled: true`.Directory where the scripts and backup files are stored (e.g. `/var/backups/mariadb`).
- `database_backup_schedule` (default: `*-*-* 05:00:00`) - systemd `OnCalendar` schedule for the timer.
- `database_backup_keep` (default: `7`)
- Number of newest backups to keep **per database**. Older dumps are removed.
- `database_backup_dbs`- List of database names to back up (e.g. `["opencast"]`).
- `database_backup_owner` / `database_backup_group`- OS user/group that owns `database_backup_output_path` and the scripts.
- `database_backup_user` / `database_backup_user_password`- MariaDB user/password used by the backup script.

### Restore script

A restore script is installed into the same output directory (next to the backup script).
It is meant to be used manually when you really want to restore a dump.

Usage:
```bash
<database_backup_output_path>/database-restore.sh <database_name> <backup_file>
```
## License

[BSD-3-Clause](LICENSE)
Expand Down
13 changes: 13 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,16 @@ database_root_user: root
opencast_mariadb_max_connections: 512
# set buffer pool size to ~80% of total memory if db runs on its own host
opencast_mariadb_innodb_buffer_pool_size: "{{ ((ansible_memtotal_mb / 1024) * 0.8) | int }}G"


# === Database backup feature (enabled) ===
database_backup_enabled: false
database_backup_output_path: ""
database_backup_schedule: "*-*-* 05:00:00" # Systemd OnCalendar format
database_backup_keep: 7
# list of databases
database_backup_dbs:
- "{{ database_name }}"
database_backup_owner: mariadbbackup
database_backup_group: mariadbbackup
database_backup_user: backup
7 changes: 3 additions & 4 deletions molecule/default/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
vars:
database_password: "1234"
database_root_password: "5678"
tasks:
- name: Include opencast_mariadb
ansible.builtin.include_role:
name: opencast_mariadb
database_backup_user_password: "5432"
roles:
- elan.opencast_mariadb
57 changes: 57 additions & 0 deletions molecule/default/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
hosts: all
vars:
database_password: "1234"
vars_files:
- ../../defaults/main.yml
tasks:
- name: Check MariaDB socket exists
ansible.builtin.wait_for:
Expand Down Expand Up @@ -42,3 +44,58 @@
ansible.builtin.fail:
msg: Opencast database does not exist
when: "query_databases.rowcount.0 != 1"

# ───────────────────────────────────────────────────────────
# Backup configuration (only when enabled)
# ───────────────────────────────────────────────────────────

- name: Verify backup configuration when enabled
when: database_backup_enabled | default(false)
block:
- name: Assert backups are enabled
ansible.builtin.assert:
that:
- database_backup_enabled | default(false)
fail_msg: "Backups are disabled; skipping backup verification."

- name: Ensure backup directory exists
ansible.builtin.stat:
path: "{{ database_backup_output_path }}"
register: backup_dir_stat

- name: Assert backup directory is present and writable
ansible.builtin.assert:
that:
- backup_dir_stat.stat.exists
- backup_dir_stat.stat.isdir
fail_msg: >
Backup directory {{ database_backup_output_path }}
is missing or not a directory.

- name: Check database-backup.service is installed and enabled
ansible.builtin.systemd:
name: database-backup.service
enabled: true
state: started

- name: Check database-backup.timer is installed and enabled
ansible.builtin.systemd:
name: database-backup.timer
enabled: true
state: started

- name: Slurp timer unit file for inspection
ansible.builtin.slurp:
path: /etc/systemd/system/database-backup.timer
register: timer_unit

- name: Assert OnCalendar line in timer unit matches schedule
ansible.builtin.assert:
that:
- "'OnCalendar={{ database_backup_schedule }}' in (timer_unit.content | b64decode)"
fail_msg: >
database-backup.timer does not contain
OnCalendar={{ database_backup_schedule }}.
success_msg: >
Timer unit file correctly contains
OnCalendar={{ database_backup_schedule }}.
87 changes: 87 additions & 0 deletions tasks/backup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
- name: Fail if backup enabled but no output path given
ansible.builtin.fail:
msg: "database_backup_output_path must be set when database_backup_enabled = true"
when:
- database_backup_enabled
- database_backup_output_path | length == 0

- name: Ensure backup OS user exists
ansible.builtin.user:
name: "{{ database_backup_owner }}"
state: present
system: true
when:
- database_backup_enabled
- database_backup_user != "root"

- name: Ensure MariaDB backup user exists for each database
community.mysql.mysql_user:
name: "{{ database_backup_user }}"
password: "{{ database_backup_user_password }}"
host: "localhost"
priv: "{{ item }}.*:SELECT,LOCK TABLES,SHOW VIEW,TRIGGER"
state: present
login_user: "{{ database_root_user }}"
login_password: "{{ database_root_password }}"
loop: "{{ database_backup_dbs }}"
when: database_backup_enabled
no_log: true

- name: Ensure backup output directory exists
ansible.builtin.file:
path: "{{ database_backup_output_path }}"
state: directory
owner: "{{ database_backup_owner }}"
group: "{{ database_backup_group }}"
mode: "0750"
when: database_backup_enabled

- name: Install backup script
ansible.builtin.template:
src: database-backup.sh.j2
dest: "{{ database_backup_output_path }}/database-backup.sh"
owner: "{{ database_backup_owner }}"
group: "{{ database_backup_group }}"
mode: "0750"
when: database_backup_enabled

- name: Install systemd service unit
ansible.builtin.template:
src: database-backup.service.j2
dest: /etc/systemd/system/database-backup.service
mode: "0644"
when: database_backup_enabled
register: database_backup_service_unit

- name: Install systemd timer unit
ansible.builtin.template:
src: database-backup.timer.j2
dest: /etc/systemd/system/database-backup.timer
mode: "0644"
when: database_backup_enabled
register: database_backup_timer_unit

- name: Reload systemd daemon (if timers changed)
ansible.builtin.systemd:
daemon_reload: true
when:
- database_backup_enabled
- database_backup_service_unit.changed or database_backup_timer_unit.changed

- name: Ensure backup timer is enabled and running
ansible.builtin.systemd:
name: database-backup.timer
enabled: true
state: started
when: database_backup_enabled

# Restore database
- name: Install restore script
ansible.builtin.template:
src: database-restore.sh.j2
dest: "{{ database_backup_output_path }}/database-restore.sh"
owner: "{{ database_backup_owner }}"
group: "{{ database_backup_group }}"
mode: "0750"
when: database_backup_enabled
7 changes: 7 additions & 0 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,10 @@
- "localhost"
- "{{ ansible_fqdn }}"
no_log: true

###############################################################################
# database backup
###############################################################################
- name: Include backup setup tasks
ansible.builtin.include_tasks: backup.yml
when: database_backup_enabled
14 changes: 14 additions & 0 deletions templates/database-backup.service.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Opencast Database Backup
After=network.target
After=local-fs.target
After=remote-fs.target

[Service]
Type=oneshot
User={{ database_backup_owner }}
Group={{ database_backup_group }}
ExecStart={{ database_backup_output_path }}/database-backup.sh

[Install]
WantedBy=multi-user.target
22 changes: 22 additions & 0 deletions templates/database-backup.sh.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

DBUSER="{{ database_backup_user }}"
DBPASS="{{ database_backup_user_password }}"
OUTDIR="{{ database_backup_output_path }}"
KEEP={{ database_backup_keep }}
DBS=({{ database_backup_dbs | join(' ') }})
TS=$(date +%Y%m%d-%H%M%S)

# Loop through each database name
for DB in "${DBS[@]}"; do
echo "Backing up $DB database to $OUTDIR/db-backup-${DB}-${TS}.sql.gz"

# Run pg_dump and compress into a .gz file
mysqldump -u "$DBUSER" -p"$DBPASS" "$DB" \
| gzip > "${OUTDIR}/db-backup-${DB}-${TS}.sql.gz"

# Remove older dumps, keep only the newest $KEEP
ls -1t "${OUTDIR}/db-backup-${DB}-"*.sql.gz \
| tail -n +$((KEEP + 1)) \
| xargs -r rm --
done
9 changes: 9 additions & 0 deletions templates/database-backup.timer.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Unit]
Description=Run database backup daily

[Timer]
OnCalendar={{ database_backup_schedule }}
Persistent=true

[Install]
WantedBy=timers.target
39 changes: 39 additions & 0 deletions templates/database-restore.sh.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

# Usage: ./database-restore.sh <database_name> <backup_file>

DB_NAME=$1
BACKUP_FILE=$2

if [ -z "$DB_NAME" ] || [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <database_name> <backup_file>"
exit 1
fi

if [ ! -f "$BACKUP_FILE" ]; then
echo "Error: Backup file $BACKUP_FILE does not exist."
exit 2
fi

DB_USER="{{ database_root_user }}"
DB_PASS="{{ database_root_password }}"

echo "Restoring database $DB_NAME from $BACKUP_FILE..."

# Drop and recreate the database
mysql -u"$DB_USER" -p"$DB_PASS" -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\`;"

# Import the backup file
if [[ "$BACKUP_FILE" == *.gz ]]; then
gunzip -c "$BACKUP_FILE" | mysql -u"$DB_USER" "-p$DB_PASS" "$DB_NAME"
else
mysql -u"$DB_USER" "-p$DB_PASS" "$DB_NAME" < "$BACKUP_FILE"
fi


if [ $? -eq 0 ]; then
echo "Restore completed successfully."
else
echo "Restore failed."
exit 3
fi