Browse Source

new: [0km] add support of component partial backup restore

Signed-off-by: Valentin Lab <valentin.lab@kalysto.org>
rc1
Valentin Lab 3 years ago
parent
commit
474cb1733e
  1. 15
      README.org
  2. 273
      bin/0km

15
README.org

@ -924,6 +924,21 @@ la source un chemin et possible aussi à la destination.
0km vps-backup recover myadmin@core-06.0k.io:10023#mail.mybackupedvps.com:/mon/chemin mynewvps.com:/ma/dest
#+end_quote
***** Récupération d'un composant
Suivant si le type de backup est reconnu et le supporte, il est
possible de nommer un composant précis en lieu et place d'un
répertoire ou d'un fichier.
Par exemple, les backup de type 'mailcow' supportent les composants
suivants: =mailcow=, =postfix=, =rspamd=, =redis=, =crypt=, =vmail=,
=vmail-attachments=, =mysql=.
#+begin_quote
0km vps-backup recover myadmin@core-06.0k.io:10023#mail.mybackupedvps.com:mailcow,mysql mynewvps.com
0km vps-backup recover myadmin@core-06.0k.io:10023#mail.mybackupedvps.com:postfix mynewvps.com
#+end_quote
** Troubleshooting

273
bin/0km

@ -386,7 +386,7 @@ EOF
stopped_containers=1
fi
if [[ -n "$path" ]]; then
if [[ "$path" == "/"* ]]; then
##
## Additional intelligence to simple file copy
@ -425,76 +425,130 @@ EOF
fi
fi
else
for vol in postfix rspamd redis crypt vmail{,-attachments}; do
volume_name="mailcowdockerized_${vol}-vol-1"
volume_dir="/var/lib/docker/volumes/${volume_name}/_data"
if ! backup:path_exists "${volume_dir}/"; then
warn "No '$volume_name' in backup. This might be expected."
continue
fi
## Create volumes if not existent
if ! ssh:run "root@$vps" -- "
[ -d '${volume_dir}' ] ||
docker run --rm -v ${volume_name}:/tmp/dummy docker.0k.io/alpine:3.9
[ -d '${volume_dir}' ]
"; then
err "Couldn't find nor create '${volume_dir}'."
ALL_TARGETS=(mailcow postfix rspamd redis crypt vmail{,-attachments} mysql)
if [[ -n "$path" ]]; then
targets=()
bad_targets=()
for target in ${path//,/ }; do
if [[ " ${ALL_TARGETS[*]} " != *" $target "* ]]; then
bad_targets+=("$target")
fi
targets+=("$target")
done
if [ "${#bad_targets[@]}" -gt 0 ]; then
bad_target_msg=$(printf "%s, " "${bad_targets[@]}")
err "Unknown components: ${bad_target_msg%, }. These are allowed components:"
printf " - %s\n" "${ALL_TARGETS[@]}" >&2
return 1
fi
echo "${WHITE}Downloading of $volume_name${NORMAL}"
backup:rsync "${volume_dir}/" "${volume_dir}" || return 1
done
## Mailcow git base
COMPOSE_FILE=
for mailcow_dir in /opt/{apps/,}mailcow-dockerized; do
backup:path_exists "${mailcow_dir}/" || continue
## this possibly change last value
COMPOSE_FILE="$mailcow_dir/docker-compose.yml"
ENV_FILE="$mailcow_dir/.env"
echo "${WHITE}Download of $mailcow_dir${NORMAL}"
backup:rsync "${mailcow_dir}"/ "${mailcow_dir}" || return 1
break
done
if [ -z "$COMPOSE_FILE" ]; then
err "Can't find mailcow base installation path in backup."
return 1
msg_target="Partial mailcow backup"
else
targets=("${ALL_TARGETS[@]}")
msg_target="Full mailcow backup"
fi
## Mysql database
echo "${WHITE}Downloading last backup of mysql backups${NORMAL}"
backup:rsync "/var/backups/mysql/" "/var/backups/mysql" || return 1
if ! env_content=$(echo "cat '$ENV_FILE'" | ssh:run "root@$vps" -- bash); then
err "Can't access env file: '$ENV_FILE'."
return 1
fi
for target in "${targets[@]}"; do
case "$target" in
postfix|rspamd|redis|crypt|vmail|vmail-attachments)
volume_name="mailcowdockerized_${target}-vol-1"
volume_dir="/var/lib/docker/volumes/${volume_name}/_data"
if ! backup:path_exists "${volume_dir}/"; then
warn "No '$volume_name' in backup. This might be expected."
continue
fi
## Create volumes if not existent
if ! ssh:run "root@$vps" -- "
[ -d '${volume_dir}' ] ||
docker run --rm -v ${volume_name}:/tmp/dummy docker.0k.io/alpine:3.9
[ -d '${volume_dir}' ]
"; then
err "Couldn't find nor create '${volume_dir}'."
return 1
fi
echo "${WHITE}Downloading of $volume_name${NORMAL}"
backup:rsync "${volume_dir}/" "${volume_dir}" || return 1
;;
mailcow)
## Mailcow git base
COMPOSE_FILE=
for mailcow_dir in /opt/{apps/,}mailcow-dockerized; do
backup:path_exists "${mailcow_dir}/" || continue
## this possibly change last value
COMPOSE_FILE="$mailcow_dir/docker-compose.yml"
ENV_FILE="$mailcow_dir/.env"
echo "${WHITE}Download of $mailcow_dir${NORMAL}"
backup:rsync "${mailcow_dir}"/ "${mailcow_dir}" || return 1
break
done
if [ -z "$COMPOSE_FILE" ]; then
err "Can't find mailcow base installation path in backup."
return 1
fi
;;
mysql)
if [ -z "$COMPOSE_FILE" ]; then
## Mailcow git base
compose_files=()
for mailcow_dir in /opt/{apps/,}mailcow-dockerized; do
ssh:run "root@$vps" -- "[ -e \"$mailcow_dir/docker-compose.yml\" ]" || continue
## this possibly change last value
compose_files+=("$mailcow_dir/docker-compose.yml")
done
if [ "${#compose_files[@]}" == 0 ]; then
err "No compose file found for mailcow installation."
return 1
elif [ "${#compose_files[@]}" -gt 1 ]; then
err "Multiple compose files for mailcow found:"
for f in "${compose_files[@]}"; do
echo " - $f" >&2
done
echo "Can't decide which to use for mounting mysql container." >&2
return 1
fi
COMPOSE_FILE="${compose_files[0]}"
ENV_FILE="${COMPOSE_FILE%/*}/.env"
if ! ssh:run "root@$vps" -- "[ -e \"${COMPOSE_FILE%/*}/.env\" ]"; then
err "No env file in '$ENV_FILE' found."
return 1
fi
fi
## Mysql database
echo "${WHITE}Downloading last backup of mysql backups${NORMAL}"
backup:rsync "/var/backups/mysql/" "/var/backups/mysql" || return 1
if ! env_content=$(echo "cat '$ENV_FILE'" | ssh:run "root@$vps" -- bash); then
err "Can't access env file: '$ENV_FILE'."
return 1
fi
root_password=$(printf "%s\n" "$env_content" | grep ^DBROOT= | cut -f 2 -d =)
root_password=$(printf "%s\n" "$env_content" | grep ^DBROOT= | cut -f 2 -d =)
echo "${WHITE}Bringing mysql-mailcow up${NORMAL}"
if ! image=$(cat <<EOF | ssh:run "root@$vps" -- bash
echo "${WHITE}Bringing mysql-mailcow up${NORMAL}"
if ! image=$(cat <<EOF | ssh:run "root@$vps" -- bash
shyaml get-value services.mysql-mailcow.image < "${COMPOSE_FILE}"
EOF
); then
err "Failed to get image name of service 'mysql-mailcow' in 'compose.yml'."
return 1
fi
if [ -z "$(ssh:run "root@$vps" -- docker images -q "$image")" ]; then
info "Image '$image' not available, pull it."
if ! ssh:run "root@$vps" -- \
docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" \
pull mysql-mailcow; then
err "Failed to pull image of service 'mysql-mailcow'."
return 1
fi
fi
if ! container_id=$(cat <<EOF | ssh:run "root@$vps" -- bash
); then
err "Failed to get image name of service 'mysql-mailcow' in 'compose.yml'."
return 1
fi
if [ -z "$(ssh:run "root@$vps" -- docker images -q "$image")" ]; then
info "Image '$image' not available, pull it."
if ! ssh:run "root@$vps" -- \
docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" \
pull mysql-mailcow; then
err "Failed to pull image of service 'mysql-mailcow'."
return 1
fi
fi
if ! container_id=$(cat <<EOF | ssh:run "root@$vps" -- bash
echo "[client]
password=$root_password" > "$VPS_TMP_DIR/my.cnf"
@ -504,35 +558,35 @@ docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" \
mysql-mailcow
EOF
); then
err "Failed to bring up mysql-mailcow"
return 1
fi
START="$SECONDS"
retries=0
timeout=600
while true; do
((retries++))
echo " waiting for mysql db..." \
"(retry $retries, $(($SECONDS - $START))s elapsed, timeout is ${timeout}s)" >&2
cat <<EOF | ssh:run "root@$vps" -- bash && break
); then
err "Failed to bring up mysql-mailcow"
return 1
fi
START="$SECONDS"
retries=0
timeout=600
while true; do
((retries++))
echo " waiting for mysql db..." \
"(retry $retries, $(($SECONDS - $START))s elapsed, timeout is ${timeout}s)" >&2
cat <<EOF | ssh:run "root@$vps" -- bash && break
echo "SELECT 1;" | docker exec -i "$container_id" mysql >/dev/null 2>&1
EOF
if (($SECONDS - $START > $timeout)); then
err "Failed to connect to mysql-mailcow."
echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
ssh:run "root@$vps" -- bash
return 1
fi
sleep 0.4
done
if (($SECONDS - $START > $timeout)); then
err "Failed to connect to mysql-mailcow."
echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
ssh:run "root@$vps" -- bash
return 1
fi
sleep 0.4
done
DBUSER=$(printf "%s\n" "$env_content" | grep ^DBUSER= | cut -f 2 -d =)
DBPASS=$(printf "%s\n" "$env_content" | grep ^DBPASS= | cut -f 2 -d =)
DBUSER=$(printf "%s\n" "$env_content" | grep ^DBUSER= | cut -f 2 -d =)
DBPASS=$(printf "%s\n" "$env_content" | grep ^DBPASS= | cut -f 2 -d =)
echo "${WHITE}Uploading mysql dump${NORMAL}"
cat <<EOF | ssh:run "root@$vps" -- bash
echo "${WHITE}Uploading mysql dump${NORMAL}"
cat <<EOF | ssh:run "root@$vps" -- bash
echo "
DROP DATABASE IF EXISTS mailcow;
@ -543,18 +597,24 @@ echo "
zcat /var/backups/mysql/mailcow/*.gz | docker exec -i "$container_id" mysql mailcow
EOF
if [ "$?" != 0 ]; then
err "Failed to load mysql dump."
echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
ssh:run "root@$vps" -- bash
return 1
fi
if [ "$?" != 0 ]; then
err "Failed to load mysql dump."
echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
ssh:run "root@$vps" -- bash
return 1
fi
echo "${WHITE}Bringing mysql-mailcow down${NORMAL}"
echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
ssh:run "root@$vps" -- bash
;;
*)
err "Unknown component '$target'. Bailing out."
return 1
esac
done
echo "${WHITE}Bringing mysql-mailcow down${NORMAL}"
echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
ssh:run "root@$vps" -- bash
ssh:run "root@$vps" -- "rm -rf '$VPS_TMP_DIR'"
msg_target="Full mailcow backup"
fi
if [ -n "$stopped_containers" ]; then
@ -619,7 +679,7 @@ vps_backup_recover() {
## from vps to backup server.
backup:setup-rsync "$admin" "$vps" "$server" "$id" || return 1
if [ -n "$path" ]; then
if [[ "$path" == "/"* ]]; then
if ! backup:path_exists "${path}"; then
err "File or directory '$path' not found in backup."
return 1
@ -640,13 +700,19 @@ vps_backup_recover() {
mailcow:vps_backup_recover "$admin" "$server" "$id" "$path" "$vps" "$vps_path"
;;
*-*)
if [ -n "$path" ]; then
if [[ "$path" == "/"* ]]; then
## For now, will require having $path and $vps_path set, no additional behaviors
file:vps_backup_recover "$admin" "$server" "$id" "$path" "$vps" "$vps_path"
else
err "Full recover of ${rtype:-unknown} backup type on" \
"${type:-unknown} type VPS is not yet implemented."
return 1
if [ -n "$path" ]; then
err "Partial component recover of ${rtype:-unknown} backup type on" \
"${type:-unknown} type VPS is not yet implemented."
return 1
else
err "Full recover of ${rtype:-unknown} backup type on" \
"${type:-unknown} type VPS is not yet implemented."
return 1
fi
fi
;;
esac
@ -1000,7 +1066,8 @@ cmdline.spec:vps-backup:cmd:recover:run() {
: :posarg: BACKUP_ID 'Backup id.
(ie: myadmin@backup.domain.org:10023#mx.myvps.org
myadmin@ark-01.org#myid:/a/path)'
myadmin@ark-01.org#myid:/a/path
admin@ark-02.io#myid:myqsl,mailcow)'
: :posarg: VPS_PATH 'Target host(s) to check.
(ie: myvps.com
myvps.com:/a/path)'

Loading…
Cancel
Save