diff --git a/README.md b/README.md index c2a6cc8..dfdce34 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ ## Longer summary -This container is still a work in progress and backups are not currently cron jobbed (a backup is only taken on re/start). Reading and using the ``docker-compose\docker-compose.yml`` and example ``.env.example`` file should be illustrative on how to use this container. See also: [Using-the-DockerHub-provided-image](#Using-the-DockerHub-provided-image) +This container is still a work in progress but the main feature (cronjob'd backups) now works. + +Reading and using the ``docker-compose\docker-compose.yml`` and example ``.env.example`` file should be illustrative on how to use this container. See also: [Using-the-DockerHub-provided-image](#Using-the-DockerHub-provided-image) ## Table of Contents diff --git a/docker-compose/.env.example b/docker-compose/.env.example index f2ea68a..4449fe0 100644 --- a/docker-compose/.env.example +++ b/docker-compose/.env.example @@ -1,7 +1,8 @@ -# The 3 variables below are required. +# The 4 variables below are required. PBS_ENCRYPTION_PASSWORD="123456789abcdefghijklmn" PBS_ENDPOINT="pbs.mydomain.com" PBS_DATASTORE="test-datastore" +CRON_SCHEDULE="0 */4 * * *" # Use of the PBS_API_KEY_NAME and PBS_API_KEY_SECRET is recommended! # If unset, ensure PBS_USER and PBS_PASSWORD are set. diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 2a55f14..38f98d9 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -13,4 +13,4 @@ services: - ./pbsconfig/:/root/.config/proxmox-backup/ - ./backups/test1:/backups/test1:ro - ./backups/test2:/backups/test2:ro - - ./backups/test3:/backups/test3:ro + - ./backups/test3:/backups/test3:ro \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index bf5f8b4..65d6cb2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,7 +7,7 @@ FROM ghcr.io/linuxserver/baseimage-debian:bookworm LABEL maintainer="Aterfax" # Get initial required packages -RUN apt-get update && apt-get install -y wget nano procps less expect +RUN apt-get update && apt-get install -y wget cron expect # Get the Proxmox signing keys and add to trust store RUN curl -o /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg && \ @@ -38,3 +38,6 @@ RUN touch etc/s6-overlay/s6-rc.d/user/contents.d/setup_check COPY ./src/s6-services/backup /etc/s6-overlay/s6-rc.d/backup RUN touch etc/s6-overlay/s6-rc.d/user/contents.d/backup + +COPY ./src/s6-services/cron-backup /etc/s6-overlay/s6-rc.d/cron-backup +RUN touch etc/s6-overlay/s6-rc.d/user/contents.d/cron-backup \ No newline at end of file diff --git a/docker/src/s6-services/backup/run b/docker/src/s6-services/backup/run index 5e39c6d..5904e94 100644 --- a/docker/src/s6-services/backup/run +++ b/docker/src/s6-services/backup/run @@ -1,9 +1,12 @@ #!/usr/bin/with-contenv bash # shellcheck shell=bash +# This script runs once as the container starts to get an immediate backup. # Define a logging function to prefix output to the docker logs. output_to_log() { - sed 's/^/[backup] /' + while IFS= read -r line; do + echo "[backup] $(date +"%Y-%m-%d %H:%M:%S") $line" + done } # Redirect all subsequent command outputs to output_to_log exec > >(output_to_log) @@ -16,63 +19,4 @@ handle_error() { } trap handle_error ERR -path_to_filename() { - local path="$1" - local path_no_trailing_slash="${path%/}" # Remove the trailing slash, if any - local path_no_first_slash="${path_no_trailing_slash#\/}" # Remove the first slash - local filename="${path_no_first_slash//\//-}" # Replace slashes with dashes - echo "${filename}" -} - -BACKUP_DIRECTORIES=() -# Iterate over each subdirectory under /backup and add its full path to the array -for dir in /backups/*; do - if [[ -d "$dir" ]]; then - BACKUP_DIRECTORIES+=("$dir/") - fi -done - -# Print the contents of the array -echo "## Detected backup directories:" -echo -e "#" -if [ ${#BACKUP_DIRECTORIES[@]} -eq 0 ]; then - echo "# Nothing to backup." -else - for path in "${BACKUP_DIRECTORIES[@]}"; do - echo -e "# $path" - done - echo -e "#\n" -fi - -# Construct the directory target list with the proxmox-backup-client syntax. -# This makes 1 .pxar file per path. -TARGETS="" -for dir in "${BACKUP_DIRECTORIES[@]}"; do - TARGET=$(path_to_filename "$dir").pxar:$dir - TARGETS="${TARGETS} ${TARGET}" -done - -# Build the backup command we want to execute. -BACKUPCMD="proxmox-backup-client backup ${TARGETS}" - -if [ -n "$PBS_DATASTORE_NS" ]; then - BACKUPCMD+=" --ns ${PBS_DATASTORE_NS}" -fi - - -# Source the variables from the setup_check scripting include file. -source /etc/s6-overlay/s6-rc.d/setup_check/run_include - -echo -e "## Backing up to repository: \n# ${PBS_REPOSITORY}\n" -echo -e "## Executing backup command: \n# ${BACKUPCMD}\n" - -if [ -n "$HEALTHCHECKSURL" ]; then - curl -fsS -m 10 --retry 5 $HEALTHCHECKSURL/start -fi - -${BACKUPCMD} 2>&1 -BACKUP_EXIT_CODE=$? - -if [ -n "$HEALTHCHECKSURL" ]; then - curl -fsS -m 10 --retry 5 ${HEALTHCHECKSURL}/${BACKUP_EXIT_CODE} -fi \ No newline at end of file +/etc/s6-overlay/s6-rc.d/backup/run_include \ No newline at end of file diff --git a/docker/src/s6-services/backup/run_include b/docker/src/s6-services/backup/run_include new file mode 100644 index 0000000..28f8af9 --- /dev/null +++ b/docker/src/s6-services/backup/run_include @@ -0,0 +1,62 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash +path_to_filename() { + local path="$1" + local path_no_trailing_slash="${path%/}" # Remove the trailing slash, if any + local path_no_first_slash="${path_no_trailing_slash#\/}" # Remove the first slash + local filename="${path_no_first_slash//\//-}" # Replace slashes with dashes + echo "${filename}" +} + +BACKUP_DIRECTORIES=() +# Iterate over each subdirectory under /backup and add its full path to the array +for dir in /backups/*; do + if [[ -d "$dir" ]]; then + BACKUP_DIRECTORIES+=("$dir/") + fi +done + +# Print the contents of the array +echo "## Detected backup directories:" +echo -e "#" +if [ ${#BACKUP_DIRECTORIES[@]} -eq 0 ]; then + echo "# Nothing to backup." +else + for path in "${BACKUP_DIRECTORIES[@]}"; do + echo -e "# $path" + done + echo -e "#\n" +fi + +# Construct the directory target list with the proxmox-backup-client syntax. +# This makes 1 .pxar file per path. +TARGETS="" +for dir in "${BACKUP_DIRECTORIES[@]}"; do + TARGET=$(path_to_filename "$dir").pxar:$dir + TARGETS="${TARGETS} ${TARGET}" +done + +# Build the backup command we want to execute. +BACKUPCMD="proxmox-backup-client backup ${TARGETS}" + +if [ -n "$PBS_DATASTORE_NS" ]; then + BACKUPCMD+=" --ns ${PBS_DATASTORE_NS}" +fi + + +# Source the variables from the setup_check scripting include file. +source /etc/s6-overlay/s6-rc.d/setup_check/run_include + +echo -e "## Backing up to repository: \n# ${PBS_REPOSITORY}\n" +echo -e "## Executing backup command: \n# ${BACKUPCMD}\n" + +if [ -n "$HEALTHCHECKSURL" ]; then + curl -fsS -m 10 --retry 5 $HEALTHCHECKSURL/start +fi + +${BACKUPCMD} 2>&1 +BACKUP_EXIT_CODE=$? + +if [ -n "$HEALTHCHECKSURL" ]; then + curl -fsS -m 10 --retry 5 ${HEALTHCHECKSURL}/${BACKUP_EXIT_CODE} +fi \ No newline at end of file diff --git a/docker/src/s6-services/cron-backup/dependencies.d/backup b/docker/src/s6-services/cron-backup/dependencies.d/backup new file mode 100644 index 0000000..e69de29 diff --git a/docker/src/s6-services/cron-backup/dependencies.d/init-os-end b/docker/src/s6-services/cron-backup/dependencies.d/init-os-end new file mode 100644 index 0000000..e69de29 diff --git a/docker/src/s6-services/cron-backup/dependencies.d/key_setup b/docker/src/s6-services/cron-backup/dependencies.d/key_setup new file mode 100644 index 0000000..e69de29 diff --git a/docker/src/s6-services/cron-backup/dependencies.d/setup_check b/docker/src/s6-services/cron-backup/dependencies.d/setup_check new file mode 100644 index 0000000..e69de29 diff --git a/docker/src/s6-services/cron-backup/run b/docker/src/s6-services/cron-backup/run new file mode 100644 index 0000000..c0b2a49 --- /dev/null +++ b/docker/src/s6-services/cron-backup/run @@ -0,0 +1,64 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash +# This script instates the cron job to take regular backups. + + +# Define a logging function to prefix output to the docker logs. +output_to_log() { + while IFS= read -r line; do + echo "[cron-backup] $(date +"%Y-%m-%d %H:%M:%S") $line" + done +} +# Redirect all subsequent command outputs to output_to_log +exec > >(output_to_log) + +# Set up error handling +handle_error() { + local exit_code="$?" + echo -e "Error occurred (Exit code: $exit_code)" + exit "$exit_code" +} +trap handle_error ERR + +validate_cron_expression() { + local cron_expression="$1" + # https://stackoverflow.com/a/57639657 + local regex='(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)*\d+|(\d+(/|-)\d+)|\d+|\*) ?){5,7})' + + if echo "$cron_expression" | grep -Pq "$regex"; then + return 0 # Valid cron expression + else + return 1 # Invalid cron expression + fi +} + +if ! validate_cron_expression "$CRON_SCHEDULE"; then + echo -e "Invalid cron expression: $CRON_SCHEDULE \n" + echo "Please define a valid cron time expression for CRON_SCHEDULE, e.g. \"*/5 * * * *\" " + sleep 60 + exit 1 +fi + +CRONLOG_FILE="/root/.config/proxmox-backup/cron.log" +CRON_FILE="/etc/cron.d/cron-backup" +CRON_LINE="${CRON_SCHEDULE} root bash -c '/etc/s6-overlay/s6-rc.d/backup/run_include' >> $CRONLOG_FILE 2>&1 " +TIMEOUT=60 + +touch "${CRONLOG_FILE}" +echo "${CRON_LINE}" > "${CRON_FILE}" +chmod +x "${CRON_FILE}" + +service cron start +echo "Cron service is now running with: \"${CRON_LINE}\" " + +# We only want new lines added, not existing log content on startup. +tail -n 0 -f "${CRONLOG_FILE}" & + +# Check if cron service is running +while :; do + if ! service cron status > /dev/null; then + echo "Error: Cron service is not running. Restarting cron." + exit 1 + fi + sleep ${TIMEOUT} +done \ No newline at end of file diff --git a/docker/src/s6-services/cron-backup/type b/docker/src/s6-services/cron-backup/type new file mode 100644 index 0000000..1780f9f --- /dev/null +++ b/docker/src/s6-services/cron-backup/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/docker/src/s6-services/cron-backup/up b/docker/src/s6-services/cron-backup/up new file mode 100644 index 0000000..6237008 --- /dev/null +++ b/docker/src/s6-services/cron-backup/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/cron-backup/run \ No newline at end of file diff --git a/docker/src/s6-services/env-test/run b/docker/src/s6-services/env-test/run index f8a49bc..9588055 100644 --- a/docker/src/s6-services/env-test/run +++ b/docker/src/s6-services/env-test/run @@ -1,9 +1,12 @@ #!/usr/bin/with-contenv bash # shellcheck shell=bash +# This script runs once as the container starts to get a snapshot of the environment variables. # Define a logging function to prefix output to the docker logs. output_to_log() { - sed 's/^/[env-test] /' + while IFS= read -r line; do + echo "[env-test] $(date +"%Y-%m-%d %H:%M:%S") $line" + done } # Redirect all subsequent command outputs to output_to_log exec > >(output_to_log) diff --git a/docker/src/s6-services/key_setup/run b/docker/src/s6-services/key_setup/run index 3c7038f..13e6c76 100644 --- a/docker/src/s6-services/key_setup/run +++ b/docker/src/s6-services/key_setup/run @@ -1,9 +1,13 @@ #!/usr/bin/with-contenv bash # shellcheck shell=bash +# This script runs once as the container starts to ensure key setup is functional +# for backups. (Either checking keys or generating new ones). # Define a logging function to prefix output to the docker logs. output_to_log() { - sed 's/^/[key_setup] /' + while IFS= read -r line; do + echo "[key_setup] $(date +"%Y-%m-%d %H:%M:%S") $line" + done } # Redirect all subsequent command outputs to output_to_log exec > >(output_to_log) diff --git a/docker/src/s6-services/setup_check/run b/docker/src/s6-services/setup_check/run index daad4c5..712f6f3 100644 --- a/docker/src/s6-services/setup_check/run +++ b/docker/src/s6-services/setup_check/run @@ -1,9 +1,13 @@ #!/usr/bin/with-contenv bash # shellcheck shell=bash +# This script runs once as the container starts to ensure variables are set +# correctly and any dependent variables also get set. # Define a logging function to prefix output to the docker logs. output_to_log() { - sed 's/^/[setup_check] /' + while IFS= read -r line; do + echo "[setup_check] $(date +"%Y-%m-%d %H:%M:%S") $line" + done } # Redirect all subsequent command outputs to output_to_log exec > >(output_to_log)