Compare commits
10 Commits
d99af2ffa9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7799e7a11f | |||
| 22fac32e46 | |||
| 3827481a0a | |||
| 3963be4ce5 | |||
| 89b86d0988 | |||
| 0f8f05cbaf | |||
| 16ca4dc37c | |||
| d9a56d178b | |||
| a6db332f5b | |||
| e0a8a89bb3 |
@@ -1,9 +1,8 @@
|
||||
# This is a shell script excerpt for configuration purposes only.
|
||||
# Handle with care! Please don't put code here, only variables.
|
||||
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
|
||||
DOIT="yes" # if empty the script makes a dry run
|
||||
# RETAIN_DAYS=7 # retains all files created within that many days
|
||||
# RETAIN_WEEKS=4 # retains one file per week/month,
|
||||
# RETAIN_MONTHS=12 # created within that many weeks/months
|
||||
|
||||
|
||||
2
.recipes/nextcloud_mariadb/.templates/crontab.nextcloud
Normal file
2
.recipes/nextcloud_mariadb/.templates/crontab.nextcloud
Normal file
@@ -0,0 +1,2 @@
|
||||
# Service-specific maintenance.
|
||||
*/5 * * * * CRON=1 USER=$LOGNAME $HOME/services/[servicename]/tools/maintenance_cron
|
||||
0
.recipes/nextcloud_mariadb/README.md
Normal file
0
.recipes/nextcloud_mariadb/README.md
Normal file
1
.recipes/nextcloud_mariadb/configs/nextcloud-config.php
Symbolic link
1
.recipes/nextcloud_mariadb/configs/nextcloud-config.php
Symbolic link
@@ -0,0 +1 @@
|
||||
../storage/volumes/nextcloud_html/config
|
||||
76
.recipes/nextcloud_mariadb/docker-compose.yml
Normal file
76
.recipes/nextcloud_mariadb/docker-compose.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
# Nextcloud Server (stable) with MariaDB LTS
|
||||
#
|
||||
services:
|
||||
# https://hub.docker.com/_/nextcloud
|
||||
# https://github.com/docker-library/docs/tree/master/nextcloud
|
||||
# https://github.com/nextcloud/server
|
||||
nextcloud:
|
||||
image: nextcloud:stable-apache
|
||||
# Database must be accessible.
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_started
|
||||
restart: unless-stopped
|
||||
# Take a look the possible public port collision.
|
||||
ports:
|
||||
- 8201:80
|
||||
links:
|
||||
- database
|
||||
volumes:
|
||||
# Needs R/O UID:GID 33:33 (www-data:www-data).
|
||||
- ./storage/volumes/nextcloud_init_d:/docker-entrypoint-hooks.d:ro
|
||||
# Needs R/W UID:GID 33:33 (www-data:www-data).
|
||||
- ./storage/volumes/nextcloud_html:/var/www/html
|
||||
- ./storage/volumes/nextcloud_data:/var/www/html/data
|
||||
environment:
|
||||
MYSQL_HOST: database
|
||||
MYSQL_DATABASE: nextcloud
|
||||
MYSQL_USER: nextcloud
|
||||
# Same as in MariaDB's configuration.
|
||||
MYSQL_PASSWORD: secret-1
|
||||
NEXTCLOUD_ADMIN_USER: admin
|
||||
# E.g. https://www.avast.com/random-password-generator
|
||||
NEXTCLOUD_ADMIN_PASSWORD: secret-3
|
||||
# Fill in according to local settings.
|
||||
#NEXTCLOUD_TRUSTED_DOMAINS: example.com cloud.example.com
|
||||
# These are required behind a reverse proxy.
|
||||
APACHE_DISABLE_REWRITE_IP: 1
|
||||
# Feel free to adapt to your Docker environment.
|
||||
TRUSTED_PROXIES: 172.16.0.0/12
|
||||
# If you force https on the reverse proxy (which is recommended).
|
||||
#OVERWRITEPROTOCOL: https
|
||||
# Optional context path.
|
||||
#OVERWRITEWEBROOT: /nextcloud
|
||||
# PHP settings.
|
||||
PHP_MEMORY_LIMIT: 1024M
|
||||
PHP_UPLOAD_LIMIT: 1024M
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: true
|
||||
#
|
||||
# https://hub.docker.com/_/mariadb
|
||||
database:
|
||||
image: mariadb:lts
|
||||
restart: unless-stopped
|
||||
# Only for migration or debugging.
|
||||
#ports:
|
||||
# - 3306:3306
|
||||
volumes:
|
||||
# Needs R/W UID:GID 999:999.
|
||||
- ./storage/volumes/mysql:/var/lib/mysql
|
||||
# Needs read UID 999.
|
||||
- ./storage/volumes/mysql_conf_d:/etc/mysql/conf.d
|
||||
- ./storage/volumes/mysql_init_d:/docker-entrypoint-initdb.d
|
||||
environment:
|
||||
MARIADB_AUTO_UPGRADE: "1"
|
||||
MARIADB_INITDB_SKIP_TZINFO: "1"
|
||||
MYSQL_DATABASE: nextcloud
|
||||
MYSQL_USER: nextcloud
|
||||
# E.g. https://www.avast.com/random-password-generator
|
||||
MYSQL_PASSWORD: secret-1
|
||||
MYSQL_ROOT_PASSWORD: secret-2
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: true
|
||||
4
.recipes/nextcloud_mariadb/storage/backups/dumps/.gitignore
vendored
Normal file
4
.recipes/nextcloud_mariadb/storage/backups/dumps/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory except these files.
|
||||
*
|
||||
!.gitignore
|
||||
!.rotate_folder.conf
|
||||
@@ -0,0 +1,8 @@
|
||||
# This is a shell script excerpt for configuration purposes only.
|
||||
# Handle with care! Please don't put code here, only variables.
|
||||
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|tar|log)$"
|
||||
DOIT="yes" # if empty the script makes a dry run
|
||||
# RETAIN_DAYS=7 # retains all files created within that many days
|
||||
# RETAIN_WEEKS=4 # retains one file per week/month,
|
||||
# RETAIN_MONTHS=12 # created within that many weeks/months
|
||||
3
.recipes/nextcloud_mariadb/storage/backups/webcontent/.gitignore
vendored
Normal file
3
.recipes/nextcloud_mariadb/storage/backups/webcontent/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except these files.
|
||||
*
|
||||
!.gitignore
|
||||
3
.recipes/nextcloud_mariadb/storage/volumes/mysql/.gitignore
vendored
Normal file
3
.recipes/nextcloud_mariadb/storage/volumes/mysql/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,30 @@
|
||||
# MariaDB 11.x settings according to the Nextcloud stack (optional).
|
||||
# See also:
|
||||
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
|
||||
# https://github.com/major/MySQLTuner-perl
|
||||
|
||||
[mysqld]
|
||||
|
||||
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
|
||||
# consider https://github.com/major/MySQLTuner-perl
|
||||
innodb_buffer_pool_size = 2G # 70-80% of memory available for MySQL/MariaDB
|
||||
innodb_buffer_pool_instances = 2 # Use 1 instance per 1GB of InnoDB pool size
|
||||
innodb_file_per_table = 1
|
||||
innodb_flush_log_at_trx_commit = 0
|
||||
innodb_flush_method = O_DIRECT
|
||||
innodb_log_buffer_size = 64M
|
||||
innodb_log_file_size = 256M # should be 25% of innodb_buffer_pool_size / 2
|
||||
innodb_stats_on_metadata = 0
|
||||
# https://mariadb.com/kb/en/configuring-mariadb-for-optimal-performance/
|
||||
# we practically don't use the MyISAM engine
|
||||
key_buffer_size = 10M
|
||||
# https://mariadb.com/kb/en/mariadb-memory-allocation/#query-cache
|
||||
query_cache_type = 0
|
||||
query_cache_size = 0
|
||||
#query_cache_type = ON
|
||||
#query_cache_size = 10M
|
||||
#query_cache_limit = 1M
|
||||
|
||||
# Buffer Settings
|
||||
#sort_buffer_size = 1M
|
||||
#join_buffer_size = 1M
|
||||
3
.recipes/nextcloud_mariadb/storage/volumes/mysql_init_d/.gitignore
vendored
Normal file
3
.recipes/nextcloud_mariadb/storage/volumes/mysql_init_d/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
3
.recipes/nextcloud_mariadb/storage/volumes/nextcloud_data/.gitignore
vendored
Normal file
3
.recipes/nextcloud_mariadb/storage/volumes/nextcloud_data/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
3
.recipes/nextcloud_mariadb/storage/volumes/nextcloud_html/.gitignore
vendored
Normal file
3
.recipes/nextcloud_mariadb/storage/volumes/nextcloud_html/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
3
.recipes/nextcloud_mariadb/storage/volumes/nextcloud_init_d/.gitignore
vendored
Normal file
3
.recipes/nextcloud_mariadb/storage/volumes/nextcloud_init_d/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
177
.recipes/nextcloud_mariadb/tools/backup.d/dumpdb_mysql.sh
Normal file
177
.recipes/nextcloud_mariadb/tools/backup.d/dumpdb_mysql.sh
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A service script to backup the docker-composed MySQL/MariaDB database.
|
||||
# Dumps database to the $BASE_DIR/storage/backups/dumps folder (by default).
|
||||
# An optional parameter may change the target folder.
|
||||
#
|
||||
# This script gets the database credentials from the docker-compose.yml file
|
||||
# and calls the mysql_dumpdb worker script which should be installed in
|
||||
# the same folder or somewhere in the path.
|
||||
#
|
||||
# Call as a Docker manager user (member of the docker Linux group) via cron.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# Kovács Zoltán <kovacs.zoltan@smartfront.hu>
|
||||
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
# 2025-02-26 v0.3
|
||||
# mod: doesn't tied to a particular composition (Mediawiki, Wordpress, etc).
|
||||
# 2024-12-01 v0.2.1
|
||||
# fix: typo in docker-compose version detection.
|
||||
# 2024-08-25 v0.2
|
||||
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
|
||||
# 2021-10-19 v0.1 Initial version.
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
|
||||
PAR_DUMPDIR=${PAR_DUMPDIR:-""} # Folder to dump within
|
||||
PAR_SERVICE=${PAR_SERVICE:-"database"} # Service's name in composition
|
||||
|
||||
# Messages (maybe overridden by configuration).
|
||||
#
|
||||
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
||||
MSG_DOESNOTRUN="This service doesn't run."
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGCONF="Fatal: missing config file"
|
||||
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
|
||||
MSG_NONWRITE="The target directory isn't writable"
|
||||
MSG_NOLOCATE="Cannot locate the database container."
|
||||
MSG_NOPARAM="Missing environment parameter"
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
CONFFILE="docker-compose.yml" # Configuration file
|
||||
DUMPDIR="storage/backups/dumps" # Folder to dump within
|
||||
USER=${USER:-LOGNAME} # Fix for cron enviroment only
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename cat cut date dirname docker \
|
||||
find grep hostname id pwd tail xargs
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Need to be root or a Docker manager user.
|
||||
#
|
||||
[[ "$USER" != 'root' ]] \
|
||||
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
||||
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
#
|
||||
# Called from the base folder (./)?
|
||||
BASE_DIR="$PAR_BASEDIR"
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute paths.
|
||||
CONFFILE="$BASE_DIR/$CONFFILE"
|
||||
DUMPDIR="${PAR_DUMPDIR:-$BASE_DIR/$DUMPDIR}"
|
||||
|
||||
# The dump target folder must be writable.
|
||||
#
|
||||
[[ ! -w "$DUMPDIR" ]] \
|
||||
&& echo "$MSG_NONWRITE: $DUMPDIR" >&2 && exit 1
|
||||
|
||||
# The composition must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Searches and parses the config file.
|
||||
#
|
||||
if [ ! -r "$CONFFILE" ]; then
|
||||
echo "$MSG_MISSINGCONF $CONFFILE" >&2; exit 1
|
||||
fi
|
||||
#
|
||||
function parse { [[ -z "$1" ]] && return
|
||||
# Gets the live lines containing the parameter.
|
||||
value=$("$CAT" "$CONFFILE" | "$GREP" -ve '^#' | \
|
||||
"$GREP" -e "$1" | "$TR" -d '\r')
|
||||
# If multiple the last one to consider.
|
||||
value=$(echo -e "$value" | "$TAIL" -n1)
|
||||
# Right side of the colon W/O leading and trailing spaces and quotes.
|
||||
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
|
||||
# Removes the trailing semicolon (if any).
|
||||
value=${value%;*}
|
||||
echo -e "$value"; return
|
||||
}
|
||||
# All parameters are mandatories.
|
||||
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
|
||||
MYDATABASE="$(parse "MYSQL_DATABASE")"
|
||||
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM MYSQL_DATABASE" >&2; exit 1; fi
|
||||
MYUSER="$(parse "MYSQL_USER")"
|
||||
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM MYSQL_USER" >&2; exit 1; fi
|
||||
MYPASSWORD="$(parse "MYSQL_PASSWORD")"
|
||||
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM MYSQL_PASSWORD" >&2; exit 1; fi
|
||||
# We've the configuration parsed.
|
||||
|
||||
# Converts the database service name to an actual running container's name.
|
||||
#
|
||||
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
|
||||
# Gives up here if failed.
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
|
||||
|
||||
# Locates the worker script (in PATH or in this script's folder).
|
||||
#
|
||||
MYSQL_DUMPDB=$(which mysql_dumpdb)
|
||||
if [ -z "$MYSQL_DUMPDB" -a -x "$("$DIRNAME" "$0")/mysql_dumpdb" ]; then
|
||||
MYSQL_DUMPDB="$SCRPATH/mysql_dumpdb"
|
||||
fi
|
||||
if [ -z "$MYSQL_DUMPDB" ]; then echo "$MSG_MISSINGDEP mysql_dumpdb."; exit 1 ; fi
|
||||
|
||||
# Tries the DB backup.
|
||||
#
|
||||
if [ -n "$MYSQL_DUMPDB" -a -w "$DUMPDIR" ]; then
|
||||
BACKUP_NAME=$MYDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
|
||||
( cd "$DUMPDIR"
|
||||
export MYCONTAINER MYUSER MYPASSWORD
|
||||
"$MYSQL_DUMPDB" --compress "$MYDATABASE" "$DUMPDIR/$BACKUP_NAME.sql" \
|
||||
2>>"$DUMPDIR/$BACKUP_NAME.log"
|
||||
)
|
||||
fi
|
||||
|
||||
# That's all, Folks! :)
|
||||
132
.recipes/nextcloud_mariadb/tools/backup.d/storage_backup.sh
Normal file
132
.recipes/nextcloud_mariadb/tools/backup.d/storage_backup.sh
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A service script to backup the relevant user's storage (apps, data, themes)
|
||||
# of a docker-composed Nextcloud instance. Creates a tarball in
|
||||
# $BASE_DIR/storage/backups/tarballs folder (by default). An optional
|
||||
# parameter may change the target folder.
|
||||
#
|
||||
# Call as a Docker manager user (member of the docker Linux group) via cron.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
# 2026-02-07 v0.2
|
||||
# new: Excludes the documents if the storage_gitbackup is active.
|
||||
# mod: custom_apps and themes folders have been added.
|
||||
# 2025-11-24 v0.1 Initial version.
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
|
||||
PAR_BACKUPDIR=${PAR_BACKUPDIR:-""} # Folder to dump within
|
||||
|
||||
# Messages (maybe overridden by configuration).
|
||||
#
|
||||
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
||||
MSG_DOESNOTRUN="This service doesn't run."
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
|
||||
MSG_NONWRITE="The target directory isn't writable"
|
||||
MSG_NOLOCATE="Cannot locate the Nextcloud container."
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
BACKUPDIR="storage/backups/tarballs" # Folder to dump within
|
||||
GITBACKUP="storage_gitbackup.sh" # Git backup utility
|
||||
SERVICENAME="nextcloud" # The composed Nextcloud service
|
||||
USER=${USER:-LOGNAME} # Fix for cron enviroment only
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename cat cut date dirname docker \
|
||||
find grep hostname id pwd tail xargs
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Need to be root or a Docker manager user.
|
||||
#
|
||||
[[ "$USER" != 'root' ]] \
|
||||
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
||||
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
#
|
||||
# Called from the base folder (./)?
|
||||
BASE_DIR="$PAR_BASEDIR"
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute paths.
|
||||
BACKUPDIR="${PAR_BACKUPDIR:-$BASE_DIR/$BACKUPDIR}"
|
||||
|
||||
# The dump target folder must be writable.
|
||||
#
|
||||
[[ ! -w "$BACKUPDIR" ]] \
|
||||
&& echo "$MSG_NONWRITE: $BACKUPDIR" >&2 && exit 1
|
||||
|
||||
# The service must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Converts the Nextcloud service name to an actual running container's name.
|
||||
#
|
||||
NCCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$SERVICENAME") | "$CUT" -c2-)"
|
||||
# Gives up here if failed.
|
||||
if [ -z "$NCCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
|
||||
|
||||
# Excludes the documents if the GITBACKUP is active.
|
||||
DOCUMENTS="data"
|
||||
[[ -n $(which "$GITBACKUP") ]] && DOCUMENTS="" # it is an executable somewhere in the path
|
||||
[[ -x "$SCRPATH/$GITBACKUP" ]] && DOCUMENTS="" # it is an executable in the current directory
|
||||
|
||||
# Tries the FS backup.
|
||||
if [ -w "$BACKUPDIR" ]; then
|
||||
BACKUP_NAME=$NCCONTAINER.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
|
||||
"$DOCKER" exec $NCCONTAINER sh \
|
||||
-c "cd /var/www/html; tar cz custom_apps $DOCUMENTS themes" \
|
||||
> "$BACKUPDIR/$BACKUP_NAME.tgz" 2>>"$BACKUPDIR/$BACKUP_NAME.log"
|
||||
fi
|
||||
|
||||
# That's all, Folks! :)
|
||||
161
.recipes/nextcloud_mariadb/tools/backup.d/storage_gitbackup.sh
Normal file
161
.recipes/nextcloud_mariadb/tools/backup.d/storage_gitbackup.sh
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Humble tool to commit the content of a docker-composed Nextcloud service's
|
||||
# web files into a git repository to make a daily backup of documents.
|
||||
# Also makes the repository with a metastore file if doesn't exist yet.
|
||||
#
|
||||
# This script called usually by the cron (but indirectly).
|
||||
# Depends loosely on metastore package, which isn't absolutely necessary,
|
||||
# but strongly recommended to backup file time attributes and permissions,
|
||||
# which the git tool doesn't do.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
|
||||
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
# 2025-11-21 v0.1 Initial release
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
BOTEMAIL=${BOTEMAIL:-"backupbot@example.com"} # Git repo owner's email (fake)
|
||||
BOTNAME=${BOTNAME:-"Backup Bot"} # Git repo owner's name (fake)
|
||||
SERVICE_BASE=${PAR_BASEDIR:-""} # Corresponding service's base
|
||||
GITDIR=${PAR_GITDIR:-""} # Folder containing .git
|
||||
BACKUPDIR=${PAR_BACKUPDIR:-""} # Folder to backup into git
|
||||
|
||||
# Basic environment settings.
|
||||
#
|
||||
LANG=C
|
||||
LC_ALL=C
|
||||
# We need also the sbin directories.
|
||||
if ! [[ "$PATH" =~ '/sbin:' ]]; then
|
||||
PATH="$PATH:/usr/local/sbin:/usr/sbin:/sbin"; fi
|
||||
|
||||
# Messages.
|
||||
#
|
||||
MSG_GITCOMMIT="Automated backup"
|
||||
MSG_MISSINGBASE="Fatal: missing SERVICE_BASE"
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGGIT="Fatal: unable to find the backup (git) folder"
|
||||
MSG_MISSINGSOURCE="Fatal: unable to find the source folder"
|
||||
MSG_WRONGGIT="Fatal: unusable backup (git) folder"
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
BACKUPPATH="storage/volumes/nextcloud_data"
|
||||
GITPATH="storage/backups/webcontent"
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in cut date dirname docker git readlink
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
#
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
#
|
||||
# Called from the base folder (./)?
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
|
||||
# Locates the folder to backup.
|
||||
#
|
||||
# Maybe given as a command line parameter.
|
||||
[[ -n "$1" ]] && SOURCEDIR="$1" && shift
|
||||
# Or as an environment variable, or fallbacks to the default.
|
||||
[[ -z "$SOURCEDIR" ]] && SOURCEDIR="${BACKUPDIR:-$SERVICE_BASE/$BACKUPPATH}"
|
||||
# Gives up here if doesn't found.
|
||||
if [ -z "$SOURCEDIR" -o ! -d "$("$READLINK" -e "$SOURCEDIR")" ]; then
|
||||
echo "$MSG_MISSINGSOURCE $SOURCEDIR"; exit 1
|
||||
fi
|
||||
|
||||
# Locates the backup (git) folder.
|
||||
#
|
||||
# Maybe given as a command line parameter.
|
||||
[[ -n "$1" ]] && GITDIR="$1" && shift
|
||||
# Or fallbacks to the default.
|
||||
[[ -z "$GITDIR" ]] && GITDIR="$SERVICE_BASE/$GITPATH"
|
||||
# Gives up here if doesn't found.
|
||||
if [ -z "$GITDIR" -o ! -d "$("$READLINK" -e "$GITDIR")" ]; then
|
||||
echo "$MSG_MISSINGGIT $GITDIR"; exit 1
|
||||
fi
|
||||
# Does it writable?
|
||||
( cd "$GITDIR" 2>/dev/null
|
||||
if [ ! "$PWD" = "$GITDIR" -o ! -w "$PWD" ]; then
|
||||
echo "$MSG_WRONGGIT $GITDIR"; exit 1
|
||||
fi
|
||||
) || exit 1
|
||||
# We've the folders localized.
|
||||
|
||||
# The service must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Attempts the backup commit.
|
||||
#
|
||||
# Initializes the git backup if it doesn't exist yet.
|
||||
if [ ! -d "$GITDIR/.git" ]; then
|
||||
# Initializes the repo itself.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" init --quiet
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.name "$BOTNAME"
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.email "$BOTEMAIL"
|
||||
fi
|
||||
# Stages all the files and non-empty folders.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add . >/dev/null
|
||||
|
||||
# Stores the file system metadata as well, if the tool has been installed.
|
||||
if [ ! -z "$(which metastore)" -a -x "$(which metastore)" ]; then
|
||||
# This commamd silently creates the metastore file if it doesnt' exist yet.
|
||||
( cd "$SOURCEDIR"
|
||||
"$(which metastore)" -smqq --file ".metadata"
|
||||
)
|
||||
# Stages it as well.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add ".metadata" >/dev/null
|
||||
fi
|
||||
# Makes the commit.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" commit --quiet -m "'$MSG_GITCOMMIT $("$DATE" '+%Y%m%d-%H%M%S')'"
|
||||
# Git done.
|
||||
|
||||
# That's all, Folks! :)
|
||||
83
.recipes/nextcloud_mariadb/tools/maintenance_cron
Executable file
83
.recipes/nextcloud_mariadb/tools/maintenance_cron
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Periodic maintenance operations for a Nextcloud instance.
|
||||
# This script is usually called by cron (perhaps indirectly).
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
# 2025-11-21 v0.1 Initial release
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder.
|
||||
|
||||
# Other initialisations.
|
||||
COMMANDLINE="php /var/www/html/cron.php"
|
||||
COMMANDUSER="www-data"
|
||||
SERVICENAME="nextcloud"
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Messages.
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
|
||||
|
||||
# Checks the dependencies.
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename dirname docker readlink
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && pwd )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && pwd )" #"
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
# Called from the base folder (./)?
|
||||
BASE_DIR="$PAR_BASEDIR"
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# On failure gives it up here silently.
|
||||
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
|
||||
# Only if the service is running.
|
||||
if [ -n "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]; then
|
||||
# Runs command COMMANDLINE on service SERVICENAME as COMMANDUSER.
|
||||
SERVICENAME="$("$BASENAME" "$BASE_DIR")-$SERVICENAME-1"
|
||||
"$DOCKER" exec -u "$COMMANDUSER" "$SERVICENAME" $COMMANDLINE
|
||||
fi
|
||||
|
||||
# That's all, Folks!
|
||||
195
.recipes/nextcloud_mariadb/tools/restoredb_mysql.sh
Normal file
195
.recipes/nextcloud_mariadb/tools/restoredb_mysql.sh
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restores a composed MySQL/MariaDB database from a dump file.
|
||||
# Gets all necessary data from the docker-compose.yml file.
|
||||
#
|
||||
# This is a wrapper script to the system-wide mysql_restoredb tool.
|
||||
# Database recovey with the necessary user management and grants
|
||||
# requires superuser privileges in MySQL, but simple data recovery
|
||||
# is possible if the user and privileges are already set.
|
||||
#
|
||||
# You have to call this script as a Docker manager user (member of the
|
||||
# 'docker' Linux group). The worker tool must be available somewhere
|
||||
# in PATH. At least 5.7.6 MySQL or at least 10.1.3 MariaDB is required.
|
||||
#
|
||||
# Usage:
|
||||
# $0 path_to_the_dumpfile [ path_to_the_service's_base ]
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
#
|
||||
# 2025-02-26 v0.1 Forked from the Smartfront repository and rewritten.
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
PAR_SERVICE=${SERVICE:-"database"} # Database container's name
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
BACKUPFOLDER="storage/backups/dumps" # Skeleton's default dump folder
|
||||
PROP_DBAPASS="MYSQL_ROOT_PASSWORD" # DB admin password property
|
||||
PROP_DBNAME="MYSQL_DATABASE" # DB name property
|
||||
PROP_DBPASS="MYSQL_PASSWORD" # DB password property
|
||||
PROP_DBUSER="MYSQL_USER" # DB username property
|
||||
USER=${USER:-LOGNAME} # Fix for cron enviroment only
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Basic environment settings.
|
||||
#
|
||||
LANG=C
|
||||
LC_ALL=C
|
||||
|
||||
# Messages.
|
||||
#
|
||||
MSG_BADDUMP="Fatal: doesn't exist or doesn't a dumpfile:"
|
||||
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
||||
MSG_DOESNOTRUN="This service doesn't run."
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGCONF="Fatal: missing config file"
|
||||
MSG_MISSINGYML="Fatal: didn't find the $YMLFILE file"
|
||||
MSG_NOLOCATE="Cannot locate the database container."
|
||||
MSG_NOPARAM="Missing environment parameter"
|
||||
|
||||
MSG_USAGE="Usage: $0 dump_pathname [ composition_base_pathname ]\n"
|
||||
MSG_USAGE+="ENVVAR:\n"
|
||||
MSG_USAGE+="SERVICE \tDatabase service's name in composition\n"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename cat cut date dirname docker \
|
||||
grep id mysql_restoredb readlink tail xargs
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Need to be root or a Docker manager user.
|
||||
#
|
||||
[[ "$USER" != 'root' ]] \
|
||||
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
||||
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
||||
|
||||
# Gets the command line parameters.
|
||||
#
|
||||
# DUMPFILE is mandatory
|
||||
if [ -n "$1" ]; then DUMPFILE="$1"; shift
|
||||
else echo -e "$MSG_USAGE" >&2; exit 1; fi
|
||||
# SERVICE_BASE is optional
|
||||
if [ -n "$1" ]; then SERVICE_BASE="$1"; shift; fi
|
||||
# We've read the unchecked command line parameters.
|
||||
|
||||
# Searches the base folder, containing the YMLFILE.
|
||||
#
|
||||
if [ -z "$SERVICE_BASE" ]; then
|
||||
# Called from the base folder (./)?
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
fi
|
||||
# On failure gives it up here.
|
||||
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute path.
|
||||
YMLFILE="$SERVICE_BASE/$YMLFILE"
|
||||
# We've the YMLFILE.
|
||||
|
||||
# Finds the DUMPFILE to use.
|
||||
#
|
||||
# The DUMPFILE must point to a readable file.
|
||||
# If doesn't it tries the skeleton's standard backup folder as well.
|
||||
if [ ! -r "$DUMPFILE" ]
|
||||
then DUMPFILE="$("$DIRNAME" "$SERVICE_BASE")/$BACKUPFOLDER/$DUMPFILE"; fi
|
||||
# If it is an existing symlink dereferences it to ensure, it points to a file.
|
||||
if [ -h "$DUMPFILE" ]; then
|
||||
if [[ "$("$READLINK" "$DUMPFILE")" != /* ]]
|
||||
# relative path in symlink
|
||||
then DUMPFILE="$("$DIRNAME" "$DUMPFILE")/$("$READLINK" "$DUMPFILE")"
|
||||
# absolute path in symlink
|
||||
else DUMPFILE="$("$READLINK" "$DUMPFILE")"; fi
|
||||
fi
|
||||
# Let's check it!
|
||||
if [ ! -r "$DUMPFILE" -o ! -f "$DUMPFILE" ]
|
||||
then echo -e "$MSG_BADDUMP $DUMPFILE"; exit 1; fi
|
||||
# We've an existing dumpfile.
|
||||
|
||||
# The composition must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Parses the YMLFILE for parameters to use.
|
||||
#
|
||||
function parse { [[ -z "$1" ]] && return
|
||||
# Gets the live lines containing the parameter.
|
||||
value=$("$CAT" "$YMLFILE" | "$GREP" -ve '^#' | \
|
||||
"$GREP" -e "^ *$1" | "$TR" -d '\r')
|
||||
# If multiple the last one to consider.
|
||||
value=$(echo -e "$value" | "$TAIL" -n1)
|
||||
# Right side of the colon W/O leading and trailing spaces and quotes.
|
||||
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
|
||||
# Removes the trailing semicolon (if any).
|
||||
value=${value%;*}
|
||||
echo -e "$value"; return
|
||||
}
|
||||
# These parameters are mandatory.
|
||||
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
|
||||
MYDATABASE="$(parse "$PROP_DBNAME")"
|
||||
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM $PROP_DBNAME" >&2; exit 1; fi
|
||||
MYUSER="$(parse "$PROP_DBUSER")"
|
||||
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM $PROP_DBUSER" >&2; exit 1; fi
|
||||
MYPASSWORD="$(parse "$PROP_DBPASS")"
|
||||
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM $PROP_DBPASS" >&2; exit 1; fi
|
||||
# These are optional.
|
||||
MYDBAUSER="root"
|
||||
MYDBAPASSWORD="$(parse "$PROP_DBAPASS")"
|
||||
# We've the configuration parsed.
|
||||
|
||||
# Converts the database service name to an actual running container's name.
|
||||
#
|
||||
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
|
||||
# Gives up here if failed.
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
|
||||
|
||||
# Calls the worker script to make the job.
|
||||
#
|
||||
export MYDBAUSER MYDBAPASSWORD MYPASSWORD
|
||||
"$MYSQL_RESTOREDB" -C "$MYCONTAINER" -U "$MYUSER" "$MYDATABASE" "$DUMPFILE"
|
||||
|
||||
# That's all, Folks! :)
|
||||
0
.recipes/redmine_mariadb/README.md
Normal file
0
.recipes/redmine_mariadb/README.md
Normal file
29
.recipes/redmine_mariadb/configs/configuration.yml
Normal file
29
.recipes/redmine_mariadb/configs/configuration.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# = Redmine configuration file
|
||||
# The entrypoint script sets chown 999:999 and chmod 644 to this file.
|
||||
#
|
||||
# Each environment has its own configuration options. If you are only
|
||||
# running in production, only the production block needs to be configured.
|
||||
# Environment specific configuration options override the default ones.
|
||||
#
|
||||
# Note that this file needs to be a valid YAML file.
|
||||
# DO NOT USE TABS! Use 2 spaces instead of tabs for indentation.
|
||||
|
||||
# default configuration options for all environments
|
||||
default:
|
||||
# Outgoing emails configuration
|
||||
# Feel free to setup your local SMTP settings here.
|
||||
email_delivery:
|
||||
delivery_method: :smtp
|
||||
smtp_settings:
|
||||
address: 'host.docker.internal'
|
||||
port: 25
|
||||
enable_starttls_auto: false
|
||||
|
||||
# specific configuration options for production environment
|
||||
# that overrides the default ones
|
||||
production:
|
||||
|
||||
# specific configuration options for development environment
|
||||
# that overrides the default ones
|
||||
development:
|
||||
|
||||
21
.recipes/redmine_mariadb/configs/database.yml
Normal file
21
.recipes/redmine_mariadb/configs/database.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
# = Redmine database configuration file
|
||||
# The entrypoint script sets chown 999:999 and chmod 644 to this file.
|
||||
#
|
||||
# Note that this file needs to be a valid YAML file.
|
||||
# DO NOT USE TABS! Use 2 spaces instead of tabs for indentation.
|
||||
|
||||
production:
|
||||
adapter: mysql2
|
||||
database: redmine
|
||||
host: database
|
||||
username: redmine
|
||||
# It should be the same as in the docker-compose.yml file.
|
||||
password: "secret-1"
|
||||
# Use "utf8" instead of "utfmb4" for MySQL prior to 5.7.7
|
||||
encoding: utf8mb4
|
||||
variables:
|
||||
# Recommended `transaction_isolation` for MySQL to avoid concurrency issues is
|
||||
# `READ-COMMITTED`.
|
||||
# In case of MySQL lower than 8, the variable name is `tx_isolation`.
|
||||
# See https://www.redmine.org/projects/redmine/wiki/MySQL_configuration
|
||||
transaction_isolation: "READ-COMMITTED"
|
||||
1
.recipes/redmine_mariadb/configs/mysql_conf_d
Symbolic link
1
.recipes/redmine_mariadb/configs/mysql_conf_d
Symbolic link
@@ -0,0 +1 @@
|
||||
../storage/volumes/mysql_conf_d
|
||||
84
.recipes/redmine_mariadb/docker-compose.yml
Normal file
84
.recipes/redmine_mariadb/docker-compose.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# Redmine (optionally with LDAP authentication) with MariaDB.
|
||||
#
|
||||
services:
|
||||
# https://hub.docker.com/_/redmine
|
||||
# https://github.com/docker-library/redmine
|
||||
# https://github.com/redmine/redmine
|
||||
redmine:
|
||||
image: redmine:6
|
||||
# Database must be accessible.
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_started
|
||||
# Below you may find a possible solution if you also need to install some of the plugins'
|
||||
# opsys dependencies. We don't want to extend the official image to maintain watchtower's
|
||||
# monitoring for updates. So we use CMD to make all the necessary changes.
|
||||
# This will slightly prolong the start of the service.
|
||||
# command:
|
||||
# - /bin/bash
|
||||
# - -c
|
||||
# - |
|
||||
# DEBIAN_FRONTEND=noninteractive apt update
|
||||
# apt install -y --no-install-recommends build-essential zlib1g-dev
|
||||
# apt clean
|
||||
# rm -rf /var/lib/apt/lists/*
|
||||
# source /docker-entrypoint.sh rails server -b 0.0.0.0
|
||||
|
||||
restart: unless-stopped
|
||||
# Take a look the possible public port collision.
|
||||
ports:
|
||||
- 8201:3000
|
||||
links:
|
||||
- database
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
# The entrypoint script sets chown 999:999 and chmod 755/644 to these below.
|
||||
# https://www.redmine.org/projects/redmine/wiki/RedmineInstall#Step-8-File-system-permissions
|
||||
- ./storage/volumes/redmine_files:/usr/src/redmine/files
|
||||
- ./configs/configuration.yml:/usr/src/redmine/config/configuration.yml
|
||||
- ./configs/database.yml:/usr/src/redmine/config/database.yml
|
||||
# Needs read UID 999.
|
||||
- ./storage/volumes/redmine_plugins:/usr/src/redmine/plugins
|
||||
environment:
|
||||
REDMINE_DB_MYSQL: database
|
||||
REDMINE_DB_DATABASE: redmine
|
||||
REDMINE_DB_USERNAME: redmine
|
||||
# Same as MYSQL_PASSWORD below.
|
||||
REDMINE_DB_PASSWORD: secret-1
|
||||
# Only if you want to disable internal DB version updates.
|
||||
#REDMINE_NO_DB_MIGRATE: 'yes'
|
||||
# Automatically follows DB version updates related to the plugins.
|
||||
REDMINE_PLUGINS_MIGRATE: 'yes'
|
||||
# https://www.avast.com/random-password-generator
|
||||
REDMINE_SECRET_KEY_BASE: secret-3
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: true
|
||||
#
|
||||
# https://hub.docker.com/_/mariadb
|
||||
database:
|
||||
image: mariadb:lts
|
||||
restart: unless-stopped
|
||||
# Only for migration or debugging.
|
||||
#ports:
|
||||
# - 3306:3306
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
# Needs R/W UID:GID 999:999.
|
||||
- ./storage/volumes/mysql:/var/lib/mysql
|
||||
# Needs read UID 999.
|
||||
- ./storage/volumes/mysql_conf_d:/etc/mysql/conf.d:ro
|
||||
- ./storage/volumes/mysql_init_d:/docker-entrypoint-initdb.d:ro
|
||||
environment:
|
||||
MARIADB_AUTO_UPGRADE: "1"
|
||||
MARIADB_INITDB_SKIP_TZINFO: "1"
|
||||
MYSQL_DATABASE: redmine
|
||||
MYSQL_USER: redmine
|
||||
# https://www.avast.com/random-password-generator
|
||||
MYSQL_PASSWORD: secret-1
|
||||
MYSQL_ROOT_PASSWORD: secret-2
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: true
|
||||
3
.recipes/redmine_mariadb/storage/backups/attachments/.gitignore
vendored
Normal file
3
.recipes/redmine_mariadb/storage/backups/attachments/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except these files.
|
||||
*
|
||||
!.gitignore
|
||||
4
.recipes/redmine_mariadb/storage/backups/dumps/.gitignore
vendored
Normal file
4
.recipes/redmine_mariadb/storage/backups/dumps/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory except these files.
|
||||
*
|
||||
!.gitignore
|
||||
!.rotate_folder.conf
|
||||
@@ -0,0 +1,8 @@
|
||||
# This is a shell script excerpt for configuration purposes only.
|
||||
# Handle with care! Please don't put code here, only variables.
|
||||
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
|
||||
DOIT="yes" # if empty the script makes a dry run
|
||||
# RETAIN_DAYS=7 # retains all files created within that many days
|
||||
# RETAIN_WEEKS=4 # retains one file per week/month,
|
||||
# RETAIN_MONTHS=12 # created within that many weeks/months
|
||||
3
.recipes/redmine_mariadb/storage/backups/plugins/latest_v6/.gitignore
vendored
Normal file
3
.recipes/redmine_mariadb/storage/backups/plugins/latest_v6/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except these files.
|
||||
*
|
||||
!.gitignore
|
||||
3
.recipes/redmine_mariadb/storage/volumes/mysql/.gitignore
vendored
Normal file
3
.recipes/redmine_mariadb/storage/volumes/mysql/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,30 @@
|
||||
# MariaDB 11.x settings according to the Redmine stack (optional).
|
||||
# See also:
|
||||
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
|
||||
# https://github.com/major/MySQLTuner-perl
|
||||
|
||||
[mysqld]
|
||||
|
||||
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
|
||||
# consider https://github.com/major/MySQLTuner-perl
|
||||
innodb_buffer_pool_size = 2G # 70-80% of memory available for MySQL/MariaDB
|
||||
innodb_buffer_pool_instances = 2 # Use 1 instance per 1GB of InnoDB pool size
|
||||
innodb_file_per_table = 1
|
||||
innodb_flush_log_at_trx_commit = 0
|
||||
innodb_flush_method = O_DIRECT
|
||||
innodb_log_buffer_size = 64M
|
||||
innodb_log_file_size = 256M # should be 25% of innodb_buffer_pool_size / 2
|
||||
innodb_stats_on_metadata = 0
|
||||
# https://mariadb.com/kb/en/configuring-mariadb-for-optimal-performance/
|
||||
# we practically don't use the MyISAM engine
|
||||
key_buffer_size = 10M
|
||||
# https://mariadb.com/kb/en/mariadb-memory-allocation/#query-cache
|
||||
query_cache_type = 0
|
||||
query_cache_size = 0
|
||||
#query_cache_type = ON
|
||||
#query_cache_size = 10M
|
||||
#query_cache_limit = 1M
|
||||
|
||||
# Buffer Settings
|
||||
#sort_buffer_size = 1M
|
||||
#join_buffer_size = 1M
|
||||
3
.recipes/redmine_mariadb/storage/volumes/mysql_init_d/.gitignore
vendored
Normal file
3
.recipes/redmine_mariadb/storage/volumes/mysql_init_d/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
3
.recipes/redmine_mariadb/storage/volumes/redmine_files/.gitignore
vendored
Normal file
3
.recipes/redmine_mariadb/storage/volumes/redmine_files/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ignore everything in this directory except this file.
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
chown -R 999:999 *
|
||||
find . -type d -exec chmod 2770 {} \;
|
||||
find . -type f -exec chmod 660 {} \;
|
||||
|
||||
177
.recipes/redmine_mariadb/tools/backup.d/dumpdb_mysql.sh
Normal file
177
.recipes/redmine_mariadb/tools/backup.d/dumpdb_mysql.sh
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A service script to backup the docker-composed MySQL/MariaDB database.
|
||||
# Dumps database to the $BASE_DIR/storage/backups/dumps folder (by default).
|
||||
# An optional parameter may change the target folder.
|
||||
#
|
||||
# This script gets the database credentials from the docker-compose.yml file
|
||||
# and calls the mysql_dumpdb worker script which should be installed in
|
||||
# the same folder or somewhere in the path.
|
||||
#
|
||||
# Call as a Docker manager user (member of the docker Linux group) via cron.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# Kovács Zoltán <kovacs.zoltan@smartfront.hu>
|
||||
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
# 2025-02-26 v0.3
|
||||
# mod: doesn't tied to a particular composition (Mediawiki, Wordpress, etc).
|
||||
# 2024-12-01 v0.2.1
|
||||
# fix: typo in docker-compose version detection.
|
||||
# 2024-08-25 v0.2
|
||||
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
|
||||
# 2021-10-19 v0.1 Initial version.
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
|
||||
PAR_DUMPDIR=${PAR_DUMPDIR:-""} # Folder to dump within
|
||||
PAR_SERVICE=${PAR_SERVICE:-"database"} # Service's name in composition
|
||||
|
||||
# Messages (maybe overridden by configuration).
|
||||
#
|
||||
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
||||
MSG_DOESNOTRUN="This service doesn't run."
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGCONF="Fatal: missing config file"
|
||||
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
|
||||
MSG_NONWRITE="The target directory isn't writable"
|
||||
MSG_NOLOCATE="Cannot locate the database container."
|
||||
MSG_NOPARAM="Missing environment parameter"
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
CONFFILE="docker-compose.yml" # Configuration file
|
||||
DUMPDIR="storage/backups/dumps" # Folder to dump within
|
||||
USER=${USER:-LOGNAME} # Fix for cron enviroment only
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename cat cut date dirname docker \
|
||||
find grep hostname id pwd tail xargs
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Need to be root or a Docker manager user.
|
||||
#
|
||||
[[ "$USER" != 'root' ]] \
|
||||
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
||||
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
#
|
||||
# Called from the base folder (./)?
|
||||
BASE_DIR="$PAR_BASEDIR"
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute paths.
|
||||
CONFFILE="$BASE_DIR/$CONFFILE"
|
||||
DUMPDIR="${PAR_DUMPDIR:-$BASE_DIR/$DUMPDIR}"
|
||||
|
||||
# The dump target folder must be writable.
|
||||
#
|
||||
[[ ! -w "$DUMPDIR" ]] \
|
||||
&& echo "$MSG_NONWRITE: $DUMPDIR" >&2 && exit 1
|
||||
|
||||
# The composition must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Searches and parses the config file.
|
||||
#
|
||||
if [ ! -r "$CONFFILE" ]; then
|
||||
echo "$MSG_MISSINGCONF $CONFFILE" >&2; exit 1
|
||||
fi
|
||||
#
|
||||
function parse { [[ -z "$1" ]] && return
|
||||
# Gets the live lines containing the parameter.
|
||||
value=$("$CAT" "$CONFFILE" | "$GREP" -ve '^#' | \
|
||||
"$GREP" -e "$1" | "$TR" -d '\r')
|
||||
# If multiple the last one to consider.
|
||||
value=$(echo -e "$value" | "$TAIL" -n1)
|
||||
# Right side of the colon W/O leading and trailing spaces and quotes.
|
||||
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
|
||||
# Removes the trailing semicolon (if any).
|
||||
value=${value%;*}
|
||||
echo -e "$value"; return
|
||||
}
|
||||
# All parameters are mandatories.
|
||||
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
|
||||
MYDATABASE="$(parse "MYSQL_DATABASE")"
|
||||
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM MYSQL_DATABASE" >&2; exit 1; fi
|
||||
MYUSER="$(parse "MYSQL_USER")"
|
||||
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM MYSQL_USER" >&2; exit 1; fi
|
||||
MYPASSWORD="$(parse "MYSQL_PASSWORD")"
|
||||
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM MYSQL_PASSWORD" >&2; exit 1; fi
|
||||
# We've the configuration parsed.
|
||||
|
||||
# Converts the database service name to an actual running container's name.
|
||||
#
|
||||
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
|
||||
# Gives up here if failed.
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
|
||||
|
||||
# Locates the worker script (in PATH or in this script's folder).
|
||||
#
|
||||
MYSQL_DUMPDB=$(which mysql_dumpdb)
|
||||
if [ -z "$MYSQL_DUMPDB" -a -x "$("$DIRNAME" "$0")/mysql_dumpdb" ]; then
|
||||
MYSQL_DUMPDB="$SCRPATH/mysql_dumpdb"
|
||||
fi
|
||||
if [ -z "$MYSQL_DUMPDB" ]; then echo "$MSG_MISSINGDEP mysql_dumpdb."; exit 1 ; fi
|
||||
|
||||
# Tries the DB backup.
|
||||
#
|
||||
if [ -n "$MYSQL_DUMPDB" -a -w "$DUMPDIR" ]; then
|
||||
BACKUP_NAME=$MYDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
|
||||
( cd "$DUMPDIR"
|
||||
export MYCONTAINER MYUSER MYPASSWORD
|
||||
"$MYSQL_DUMPDB" --compress "$MYDATABASE" "$DUMPDIR/$BACKUP_NAME.sql" \
|
||||
2>>"$DUMPDIR/$BACKUP_NAME.log"
|
||||
)
|
||||
fi
|
||||
|
||||
# That's all, Folks! :)
|
||||
132
.recipes/redmine_mariadb/tools/backup.d/storage_backup.sh
Normal file
132
.recipes/redmine_mariadb/tools/backup.d/storage_backup.sh
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A service script to backup the relevant storage (config, files, plugins)
|
||||
# of a docker-composed Redmine instance. Creates a tarball in
|
||||
# $BASE_DIR/storage/backups/tarballs folder (by default). An optional
|
||||
# parameter may change the target folder.
|
||||
#
|
||||
# Call as a Docker manager user (member of the docker Linux group) via cron.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
|
||||
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
# 2024-09-10 v0.2
|
||||
# new: Excludes the documents if the storage_gitbackup is active.
|
||||
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
|
||||
# 2021-09-01 v0.1 Initial version.
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
|
||||
PAR_BACKUPDIR=${PAR_BACKUPDIR:-""} # Folder to dump within
|
||||
|
||||
# Messages (maybe overridden by configuration).
|
||||
#
|
||||
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
||||
MSG_DOESNOTRUN="This service doesn't run."
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
|
||||
MSG_NONWRITE="The target directory isn't writable"
|
||||
MSG_NOLOCATE="Cannot locate the Redmine container."
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
BACKUPDIR="storage/backups/tarballs" # Folder to dump within
|
||||
GITBACKUP="storage_gitbackup.sh" # Git backup utility
|
||||
SERVICENAME="redmine" # The composed Redmine service
|
||||
USER=${USER:-LOGNAME} # Fix for cron enviroment only
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename cat cut date dirname docker \
|
||||
find grep hostname id pwd tail xargs
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Need to be root or a Docker manager user.
|
||||
#
|
||||
[[ "$USER" != 'root' ]] \
|
||||
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
||||
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
#
|
||||
# Called from the base folder (./)?
|
||||
BASE_DIR="$PAR_BASEDIR"
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute paths.
|
||||
BACKUPDIR="${PAR_BACKUPDIR:-$BASE_DIR/$BACKUPDIR}"
|
||||
|
||||
# The dump target folder must be writable.
|
||||
#
|
||||
[[ ! -w "$BACKUPDIR" ]] \
|
||||
&& echo "$MSG_NONWRITE: $BACKUPDIR" >&2 && exit 1
|
||||
|
||||
# The service must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Converts the Redmine service name to an actual running container's name.
|
||||
#
|
||||
RMCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$SERVICENAME") | "$CUT" -c2-)"
|
||||
# Gives up here if failed.
|
||||
if [ -z "$RMCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
|
||||
|
||||
# Excludes the documents if the GITBACKUP is active.
|
||||
DOCUMENTS="files"
|
||||
[[ -n $(which "$GITBACKUP") ]] && DOCUMENTS="" # it is an executable somewhere in the path
|
||||
[[ -x "$SCRPATH/$GITBACKUP" ]] && DOCUMENTS="" # it is an executable in the current directory
|
||||
|
||||
# Tries the FS backup.
|
||||
if [ -w "$BACKUPDIR" ]; then
|
||||
BACKUP_NAME=$RMCONTAINER.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
|
||||
"$DOCKER" exec $RMCONTAINER sh \
|
||||
-c "cd /usr/src/redmine; tar cz config $DOCUMENTS plugins" \
|
||||
> "$BACKUPDIR/$BACKUP_NAME.tgz" 2>>"$BACKUPDIR/$BACKUP_NAME.log"
|
||||
fi
|
||||
|
||||
# That's all, Folks! :)
|
||||
164
.recipes/redmine_mariadb/tools/backup.d/storage_gitbackup.sh
Normal file
164
.recipes/redmine_mariadb/tools/backup.d/storage_gitbackup.sh
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Humble tool to commit the content of a docker-composed Redmine service's
|
||||
# attachments into a git repository to make a daily backup of documents.
|
||||
# Also makes the repository with a metastore file if doesn't exist yet.
|
||||
#
|
||||
# This script called usually by the cron (but indirectly).
|
||||
# Depends loosely on metastore package, which isn't absolutely necessary,
|
||||
# but strongly recommended to backup file time attributes and permissions,
|
||||
# which the git tool doesn't do.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
# 2025-01-18 v0.3
|
||||
# fix: a typo (BASE_DIR instead of SERVICE_BASE).
|
||||
# 2024-09-10 v0.2
|
||||
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
|
||||
# mod: Doesn't do backup if the service is down.
|
||||
# 2021-11-21 v0.1 Initial release
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
BOTEMAIL=${BOTEMAIL:-"backupbot@example.com"} # Git repo owner's email (fake)
|
||||
BOTNAME=${BOTNAME:-"Backup Bot"} # Git repo owner's name (fake)
|
||||
SERVICE_BASE=${PAR_BASEDIR:-""} # Corresponding service's base
|
||||
GITDIR=${PAR_GITDIR:-""} # Folder containing .git
|
||||
SOURCEDIR=${PAR_BACKUPDIR:-""} # Folder to backup into git
|
||||
|
||||
# Basic environment settings.
|
||||
#
|
||||
LANG=C
|
||||
LC_ALL=C
|
||||
# We need also the sbin directories.
|
||||
if ! [[ "$PATH" =~ '/sbin:' ]]; then
|
||||
PATH="$PATH:/usr/local/sbin:/usr/sbin:/sbin"; fi
|
||||
|
||||
# Messages.
|
||||
#
|
||||
MSG_GITCOMMIT="Automated backup"
|
||||
MSG_MISSINGBASE="Fatal: missing SERVICE_BASE"
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGGIT="Fatal: unable to find the backup (git) folder"
|
||||
MSG_MISSINGSOURCE="Fatal: unable to find the source folder"
|
||||
MSG_WRONGGIT="Fatal: unusable backup (git) folder"
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
GITPATH="storage/backups/attachments"
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in cut date dirname docker git readlink
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
#
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
#
|
||||
# Called from the base folder (./)?
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute paths.
|
||||
BACKUPDIR="${PAR_BACKUPDIR:-$SERVICE_BASE/$BACKUPDIR}"
|
||||
|
||||
# Locates the source folder.
|
||||
#
|
||||
# Maybe given as a command line parameter.
|
||||
[[ -n "$1" ]] && SOURCEDIR="$1" && shift
|
||||
[[ -z "$SOURCEDIR" ]] && SOURCEDIR="$SERVICE_BASE/storage/volumes/redmine_files"
|
||||
# Gives up here if doesn't found.
|
||||
if [ -z "$SOURCEDIR" -o ! -d "$("$READLINK" -e "$SOURCEDIR")" ]; then
|
||||
echo "$MSG_MISSINGSOURCE $SOURCEDIR"; exit 1
|
||||
fi
|
||||
|
||||
# Locates the backup (git) folder.
|
||||
#
|
||||
# Maybe given as a command line parameter.
|
||||
[[ -n "$1" ]] && GITDIR="$1" && shift
|
||||
[[ -z "$GITDIR" ]] && GITDIR="$SERVICE_BASE/$GITPATH"
|
||||
# Gives up here if doesn't found.
|
||||
if [ -z "$GITDIR" -o ! -d "$("$READLINK" -e "$GITDIR")" ]; then
|
||||
echo "$MSG_MISSINGGIT $GITDIR"; exit 1
|
||||
fi
|
||||
# Does it writable?
|
||||
( cd "$GITDIR" 2>/dev/null
|
||||
if [ ! "$PWD" = "$GITDIR" -o ! -w "$PWD" ]; then
|
||||
echo "$MSG_WRONGGIT $GITDIR"; exit 1
|
||||
fi
|
||||
) || exit 1
|
||||
# We've the folders localized.
|
||||
|
||||
# The service must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Attempts the backup commit.
|
||||
#
|
||||
# Initializes the git backup if it doesn't exist yet.
|
||||
if [ ! -d "$GITDIR/.git" ]; then
|
||||
# Initializes the repo itself.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" init --quiet
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.name "$BOTNAME"
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.email "$BOTEMAIL"
|
||||
fi
|
||||
# Stages all the files and non-empty folders.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add . >/dev/null
|
||||
# Stores the file system metadata as well, if the tool has been installed.
|
||||
if [ ! -z "$(which metastore)" -a -x "$(which metastore)" ]; then
|
||||
# This commamd silently creates the metastore file if it doesnt' exist yet.
|
||||
( cd "$SOURCEDIR"
|
||||
"$(which metastore)" -smqq --file ".metadata"
|
||||
)
|
||||
# Stages it as well.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add ".metadata" >/dev/null
|
||||
fi
|
||||
# Makes the commit.
|
||||
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" commit --quiet -m "'$MSG_GITCOMMIT $("$DATE" '+%Y%m%d-%H%M%S')'"
|
||||
# Git done.
|
||||
|
||||
# That's all, Folks! :)
|
||||
24
.recipes/redmine_mariadb/tools/download_plugins_v6.sh
Normal file
24
.recipes/redmine_mariadb/tools/download_plugins_v6.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Downloads a copy of the latest versions of the installed plugins
|
||||
# into a temporary destination folder. Doesn't hurt the running
|
||||
# instance.
|
||||
#
|
||||
# To perform the actual upgrade you have to stop the instance and manually
|
||||
# upgrade the contents of the storage/volumes/redmine_plugins folder.
|
||||
|
||||
# Where I'm?
|
||||
SCRPATH="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #"
|
||||
# Temporary destination folder for download.
|
||||
PLUGINS=$(readlink -f "$SCRPATH/../storage/backups/plugins/latest_v6")
|
||||
if [ ! -d "$PLUGINS" -o ! -w "$PLUGINS" ]; then
|
||||
echo -e "Bad destination: $PLUGINS" >&2; exit 1
|
||||
fi
|
||||
|
||||
# Plugins from GitHub.
|
||||
# Feel free to adjust it to your needs.
|
||||
#rm -rf "$PLUGINS/additionals" 2>/dev/null
|
||||
#git clone -b stable https://github.com/alphanodes/additionals.git "$PLUGINS/additionals"
|
||||
|
||||
# Some cleanup
|
||||
rm -rf "$PLUGINS"/*/.git "$PLUGINS"/*/.github
|
||||
195
.recipes/redmine_mariadb/tools/restoredb_mysql.sh
Normal file
195
.recipes/redmine_mariadb/tools/restoredb_mysql.sh
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restores a composed MySQL/MariaDB database from a dump file.
|
||||
# Gets all necessary data from the docker-compose.yml file.
|
||||
#
|
||||
# This is a wrapper script to the system-wide mysql_restoredb tool.
|
||||
# Database recovey with the necessary user management and grants
|
||||
# requires superuser privileges in MySQL, but simple data recovery
|
||||
# is possible if the user and privileges are already set.
|
||||
#
|
||||
# You have to call this script as a Docker manager user (member of the
|
||||
# 'docker' Linux group). The worker tool must be available somewhere
|
||||
# in PATH. At least 5.7.6 MySQL or at least 10.1.3 MariaDB is required.
|
||||
#
|
||||
# Usage:
|
||||
# $0 path_to_the_dumpfile [ path_to_the_service's_base ]
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
#
|
||||
# 2025-02-26 v0.1 Forked from the Smartfront repository and rewritten.
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
#
|
||||
PAR_SERVICE=${SERVICE:-"database"} # Database container's name
|
||||
|
||||
# Other initialisations.
|
||||
#
|
||||
BACKUPFOLDER="storage/backups/dumps" # Skeleton's default dump folder
|
||||
PROP_DBAPASS="MYSQL_ROOT_PASSWORD" # DB admin password property
|
||||
PROP_DBNAME="MYSQL_DATABASE" # DB name property
|
||||
PROP_DBPASS="MYSQL_PASSWORD" # DB password property
|
||||
PROP_DBUSER="MYSQL_USER" # DB username property
|
||||
USER=${USER:-LOGNAME} # Fix for cron enviroment only
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Basic environment settings.
|
||||
#
|
||||
LANG=C
|
||||
LC_ALL=C
|
||||
|
||||
# Messages.
|
||||
#
|
||||
MSG_BADDUMP="Fatal: doesn't exist or doesn't a dumpfile:"
|
||||
MSG_DOCKERGRPNEED="You must be a member of the docker group."
|
||||
MSG_DOESNOTRUN="This service doesn't run."
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGCONF="Fatal: missing config file"
|
||||
MSG_MISSINGYML="Fatal: didn't find the $YMLFILE file"
|
||||
MSG_NOLOCATE="Cannot locate the database container."
|
||||
MSG_NOPARAM="Missing environment parameter"
|
||||
|
||||
MSG_USAGE="Usage: $0 dump_pathname [ composition_base_pathname ]\n"
|
||||
MSG_USAGE+="ENVVAR:\n"
|
||||
MSG_USAGE+="SERVICE \tDatabase service's name in composition\n"
|
||||
|
||||
# Checks the dependencies.
|
||||
#
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename cat cut date dirname docker \
|
||||
grep id mysql_restoredb readlink tail xargs
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
#
|
||||
# Let's find which version of docker-compose is installed.
|
||||
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
|
||||
# We'll use v2 if it is available.
|
||||
DOCKER_COMPOSE="$DOCKER"
|
||||
commandstring="compose"
|
||||
else
|
||||
# Otherwise falling back to v1.
|
||||
DOCKER_COMPOSE="$(which docker-compose)"
|
||||
commandstring=""
|
||||
fi
|
||||
# One of the two is mandatory.
|
||||
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
|
||||
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="$0"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
|
||||
|
||||
# Need to be root or a Docker manager user.
|
||||
#
|
||||
[[ "$USER" != 'root' ]] \
|
||||
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
|
||||
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
|
||||
|
||||
# Gets the command line parameters.
|
||||
#
|
||||
# DUMPFILE is mandatory
|
||||
if [ -n "$1" ]; then DUMPFILE="$1"; shift
|
||||
else echo -e "$MSG_USAGE" >&2; exit 1; fi
|
||||
# SERVICE_BASE is optional
|
||||
if [ -n "$1" ]; then SERVICE_BASE="$1"; shift; fi
|
||||
# We've read the unchecked command line parameters.
|
||||
|
||||
# Searches the base folder, containing the YMLFILE.
|
||||
#
|
||||
if [ -z "$SERVICE_BASE" ]; then
|
||||
# Called from the base folder (./)?
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
|
||||
fi
|
||||
# On failure gives it up here.
|
||||
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
# Sets the absolute path.
|
||||
YMLFILE="$SERVICE_BASE/$YMLFILE"
|
||||
# We've the YMLFILE.
|
||||
|
||||
# Finds the DUMPFILE to use.
|
||||
#
|
||||
# The DUMPFILE must point to a readable file.
|
||||
# If doesn't it tries the skeleton's standard backup folder as well.
|
||||
if [ ! -r "$DUMPFILE" ]
|
||||
then DUMPFILE="$("$DIRNAME" "$SERVICE_BASE")/$BACKUPFOLDER/$DUMPFILE"; fi
|
||||
# If it is an existing symlink dereferences it to ensure, it points to a file.
|
||||
if [ -h "$DUMPFILE" ]; then
|
||||
if [[ "$("$READLINK" "$DUMPFILE")" != /* ]]
|
||||
# relative path in symlink
|
||||
then DUMPFILE="$("$DIRNAME" "$DUMPFILE")/$("$READLINK" "$DUMPFILE")"
|
||||
# absolute path in symlink
|
||||
else DUMPFILE="$("$READLINK" "$DUMPFILE")"; fi
|
||||
fi
|
||||
# Let's check it!
|
||||
if [ ! -r "$DUMPFILE" -o ! -f "$DUMPFILE" ]
|
||||
then echo -e "$MSG_BADDUMP $DUMPFILE"; exit 1; fi
|
||||
# We've an existing dumpfile.
|
||||
|
||||
# The composition must be running - silently gives up here if not.
|
||||
#
|
||||
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
|
||||
&& exit 1
|
||||
|
||||
# Parses the YMLFILE for parameters to use.
|
||||
#
|
||||
function parse { [[ -z "$1" ]] && return
|
||||
# Gets the live lines containing the parameter.
|
||||
value=$("$CAT" "$YMLFILE" | "$GREP" -ve '^#' | \
|
||||
"$GREP" -e "^ *$1" | "$TR" -d '\r')
|
||||
# If multiple the last one to consider.
|
||||
value=$(echo -e "$value" | "$TAIL" -n1)
|
||||
# Right side of the colon W/O leading and trailing spaces and quotes.
|
||||
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
|
||||
# Removes the trailing semicolon (if any).
|
||||
value=${value%;*}
|
||||
echo -e "$value"; return
|
||||
}
|
||||
# These parameters are mandatory.
|
||||
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
|
||||
MYDATABASE="$(parse "$PROP_DBNAME")"
|
||||
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM $PROP_DBNAME" >&2; exit 1; fi
|
||||
MYUSER="$(parse "$PROP_DBUSER")"
|
||||
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM $PROP_DBUSER" >&2; exit 1; fi
|
||||
MYPASSWORD="$(parse "$PROP_DBPASS")"
|
||||
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM $PROP_DBPASS" >&2; exit 1; fi
|
||||
# These are optional.
|
||||
MYDBAUSER="root"
|
||||
MYDBAPASSWORD="$(parse "$PROP_DBAPASS")"
|
||||
# We've the configuration parsed.
|
||||
|
||||
# Converts the database service name to an actual running container's name.
|
||||
#
|
||||
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
|
||||
# Gives up here if failed.
|
||||
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
|
||||
|
||||
# Calls the worker script to make the job.
|
||||
#
|
||||
export MYDBAUSER MYDBAPASSWORD MYPASSWORD
|
||||
"$MYSQL_RESTOREDB" -C "$MYCONTAINER" -U "$MYUSER" "$MYDATABASE" "$DUMPFILE"
|
||||
|
||||
# That's all, Folks! :)
|
||||
@@ -1,9 +1,8 @@
|
||||
# This is a shell script excerpt for configuration purposes only.
|
||||
# Handle with care! Please don't put code here, only variables.
|
||||
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
|
||||
DOIT="yes" # if empty the script makes a dry run
|
||||
# RETAIN_DAYS=7 # retains all files created within that many days
|
||||
# RETAIN_WEEKS=4 # retains one file per week/month,
|
||||
# RETAIN_MONTHS=12 # created within that many weeks/months
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
VER=3.0.8
|
||||
VER=3.1.3
|
||||
|
||||
PROJECT_NAME="acme.sh"
|
||||
|
||||
@@ -23,9 +23,6 @@ _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY"
|
||||
CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory"
|
||||
CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
|
||||
CA_BUYPASS="https://api.buypass.com/acme/directory"
|
||||
CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory"
|
||||
|
||||
CA_ZEROSSL="https://acme.zerossl.com/v2/DV90"
|
||||
_ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email"
|
||||
|
||||
@@ -35,6 +32,8 @@ CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc"
|
||||
CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory"
|
||||
CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory"
|
||||
|
||||
CA_ACTALIS="https://acme-api.actalis.com/acme/directory"
|
||||
|
||||
DEFAULT_CA=$CA_ZEROSSL
|
||||
DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST
|
||||
|
||||
@@ -42,14 +41,13 @@ CA_NAMES="
|
||||
ZeroSSL.com,zerossl
|
||||
LetsEncrypt.org,letsencrypt
|
||||
LetsEncrypt.org_test,letsencrypt_test,letsencrypttest
|
||||
BuyPass.com,buypass
|
||||
BuyPass.com_test,buypass_test,buypasstest
|
||||
SSL.com,sslcom
|
||||
Google.com,google
|
||||
Google.com_test,googletest,google_test
|
||||
Actalis.com,actalis.com,actalis
|
||||
"
|
||||
|
||||
CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST"
|
||||
CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST,$CA_ACTALIS"
|
||||
|
||||
DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)"
|
||||
|
||||
@@ -67,7 +65,7 @@ ID_TYPE_IP="ip"
|
||||
|
||||
LOCAL_ANY_ADDRESS="0.0.0.0"
|
||||
|
||||
DEFAULT_RENEW=60
|
||||
DEFAULT_RENEW=30
|
||||
|
||||
NO_VALUE="no"
|
||||
|
||||
@@ -180,6 +178,8 @@ _VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity"
|
||||
|
||||
_DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck"
|
||||
|
||||
_PROFILESELECTION_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Profile-selection"
|
||||
|
||||
_DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead."
|
||||
|
||||
_DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR"
|
||||
@@ -250,6 +250,13 @@ _dlg_versions() {
|
||||
socat -V 2>&1
|
||||
else
|
||||
_debug "socat doesn't exist."
|
||||
if _exists "python3"; then
|
||||
python3 -V 2>&1
|
||||
elif _exists "python2"; then
|
||||
python2 -V 2>&1
|
||||
elif _exists "python"; then
|
||||
python -V 2>&1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -436,14 +443,28 @@ _secure_debug3() {
|
||||
fi
|
||||
}
|
||||
|
||||
__USE_TR_TAG=""
|
||||
if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ]; then
|
||||
__USE_TR_TAG="1"
|
||||
fi
|
||||
export __USE_TR_TAG
|
||||
|
||||
_upper_case() {
|
||||
# shellcheck disable=SC2018,SC2019
|
||||
tr '[a-z]' '[A-Z]'
|
||||
if [ "$__USE_TR_TAG" ]; then
|
||||
LANG=C tr '[:lower:]' '[:upper:]'
|
||||
else
|
||||
# shellcheck disable=SC2018,SC2019
|
||||
LANG=C tr '[a-z]' '[A-Z]'
|
||||
fi
|
||||
}
|
||||
|
||||
_lower_case() {
|
||||
# shellcheck disable=SC2018,SC2019
|
||||
tr '[A-Z]' '[a-z]'
|
||||
if [ "$__USE_TR_TAG" ]; then
|
||||
LANG=C tr '[:upper:]' '[:lower:]'
|
||||
else
|
||||
# shellcheck disable=SC2018,SC2019
|
||||
LANG=C tr '[A-Z]' '[a-z]'
|
||||
fi
|
||||
}
|
||||
|
||||
_startswith() {
|
||||
@@ -672,8 +693,10 @@ _hex_dump() {
|
||||
#0 1 2 3 4 5 6 7 8 9 - _ . ~
|
||||
#30 31 32 33 34 35 36 37 38 39 2d 5f 2e 7e
|
||||
|
||||
#_url_encode [upper-hex] the encoded hex will be upper-case if the argument upper-hex is followed
|
||||
#stdin stdout
|
||||
_url_encode() {
|
||||
_upper_hex=$1
|
||||
_hex_str=$(_hex_dump)
|
||||
_debug3 "_url_encode"
|
||||
_debug3 "_hex_str" "$_hex_str"
|
||||
@@ -883,6 +906,9 @@ _url_encode() {
|
||||
;;
|
||||
#other hex
|
||||
*)
|
||||
if [ "$_upper_hex" = "upper-hex" ]; then
|
||||
_hex_code=$(printf "%s" "$_hex_code" | _upper_case)
|
||||
fi
|
||||
printf '%%%s' "$_hex_code"
|
||||
;;
|
||||
esac
|
||||
@@ -916,6 +942,9 @@ _sed_i() {
|
||||
if sed -h 2>&1 | grep "\-i\[SUFFIX]" >/dev/null 2>&1; then
|
||||
_debug "Using sed -i"
|
||||
sed -i "$options" "$filename"
|
||||
elif sed -h 2>&1 | grep "\-i extension" >/dev/null 2>&1; then
|
||||
_debug "Using FreeBSD sed -i"
|
||||
sed -i "" "$options" "$filename"
|
||||
else
|
||||
_debug "No -i support in sed"
|
||||
text="$(cat "$filename")"
|
||||
@@ -1009,7 +1038,7 @@ _digest() {
|
||||
|
||||
outputhex="$2"
|
||||
|
||||
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
|
||||
if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
|
||||
if [ "$outputhex" ]; then
|
||||
${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
|
||||
else
|
||||
@@ -1228,7 +1257,7 @@ _idn() {
|
||||
fi
|
||||
}
|
||||
|
||||
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1
|
||||
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage
|
||||
_createcsr() {
|
||||
_debug _createcsr
|
||||
domain="$1"
|
||||
@@ -1237,6 +1266,7 @@ _createcsr() {
|
||||
csr="$4"
|
||||
csrconf="$5"
|
||||
acmeValidationv1="$6"
|
||||
extusage="$7"
|
||||
_debug2 domain "$domain"
|
||||
_debug2 domainlist "$domainlist"
|
||||
_debug2 csrkey "$csrkey"
|
||||
@@ -1245,9 +1275,8 @@ _createcsr() {
|
||||
|
||||
printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf"
|
||||
|
||||
if [ "$Le_ExtKeyUse" ]; then
|
||||
_savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse"
|
||||
printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf"
|
||||
if [ "$extusage" ]; then
|
||||
printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf"
|
||||
else
|
||||
printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf"
|
||||
fi
|
||||
@@ -1393,6 +1422,12 @@ _ss() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(uname)" = "AIX" ]; then
|
||||
_debug "Using: AIX netstat"
|
||||
netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port "
|
||||
return 0
|
||||
fi
|
||||
|
||||
if _exists "netstat"; then
|
||||
_debug "Using: netstat"
|
||||
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
|
||||
@@ -1437,8 +1472,8 @@ _toPkcs() {
|
||||
else
|
||||
${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca"
|
||||
fi
|
||||
if [ "$?" == "0" ]; then
|
||||
_savedomainconf "Le_PFXPassword" "$pfxPassword"
|
||||
if [ "$?" = "0" ]; then
|
||||
_savedomainconf "Le_PFXPassword" "$pfxPassword" "base64"
|
||||
fi
|
||||
|
||||
}
|
||||
@@ -1623,6 +1658,11 @@ _time2str() {
|
||||
return
|
||||
fi
|
||||
|
||||
#Omnios
|
||||
if date -u -r "$1" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
|
||||
#Solaris
|
||||
if printf "%(%Y-%m-%dT%H:%M:%SZ)T\n" $1 2>/dev/null; then
|
||||
return
|
||||
@@ -1792,6 +1832,10 @@ _time() {
|
||||
# 2022-04-01 08:10:33 to 1648800633
|
||||
#or 2022-04-01T08:10:33Z to 1648800633
|
||||
_date2time() {
|
||||
#Mac/BSD
|
||||
if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
#Linux
|
||||
if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
|
||||
return
|
||||
@@ -1801,12 +1845,12 @@ _date2time() {
|
||||
if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
#Mac/BSD
|
||||
if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
|
||||
#Omnios
|
||||
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
#Omnios
|
||||
if da="$(echo "$1" | tr -d "Z" | tr "T" ' ')" perl -MTime::Piece -e 'print Time::Piece->strptime($ENV{da}, "%Y-%m-%d %H:%M:%S")->epoch, "\n";' 2>/dev/null; then
|
||||
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
_err "Cannot parse _date2time $1"
|
||||
@@ -1860,6 +1904,11 @@ _inithttp() {
|
||||
|
||||
if [ -z "$_ACME_CURL" ] && _exists "curl"; then
|
||||
_ACME_CURL="curl --silent --dump-header $HTTP_HEADER "
|
||||
if [ "$ACME_USE_IPV6_REQUESTS" ]; then
|
||||
_ACME_CURL="$_ACME_CURL --ipv6 "
|
||||
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
|
||||
_ACME_CURL="$_ACME_CURL --ipv4 "
|
||||
fi
|
||||
if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then
|
||||
_ACME_CURL="$_ACME_CURL -L "
|
||||
fi
|
||||
@@ -1887,6 +1936,11 @@ _inithttp() {
|
||||
|
||||
if [ -z "$_ACME_WGET" ] && _exists "wget"; then
|
||||
_ACME_WGET="wget -q"
|
||||
if [ "$ACME_USE_IPV6_REQUESTS" ]; then
|
||||
_ACME_WGET="$_ACME_WGET --inet6-only "
|
||||
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
|
||||
_ACME_WGET="$_ACME_WGET --inet4-only "
|
||||
fi
|
||||
if [ "$ACME_HTTP_NO_REDIRECTS" ]; then
|
||||
_ACME_WGET="$_ACME_WGET --max-redirect 0 "
|
||||
fi
|
||||
@@ -2188,7 +2242,6 @@ _send_signed_request() {
|
||||
_debug2 _headers "$_headers"
|
||||
_CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)"
|
||||
fi
|
||||
_debug2 _CACHED_NONCE "$_CACHED_NONCE"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Cannot connect to $nonceurl to get nonce."
|
||||
return 1
|
||||
@@ -2305,6 +2358,7 @@ _setopt() {
|
||||
fi
|
||||
if [ ! -f "$__conf" ]; then
|
||||
touch "$__conf"
|
||||
chmod 600 "$__conf"
|
||||
fi
|
||||
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
|
||||
echo >>"$__conf"
|
||||
@@ -2361,7 +2415,7 @@ _clear_conf() {
|
||||
_sdkey="$2"
|
||||
if [ "$_c_c_f" ]; then
|
||||
_conf_data="$(cat "$_c_c_f")"
|
||||
echo "$_conf_data" | sed "s/^$_sdkey *=.*$//" >"$_c_c_f"
|
||||
echo "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
|
||||
else
|
||||
_err "Config file is empty, cannot clear"
|
||||
fi
|
||||
@@ -2513,37 +2567,76 @@ _startserver() {
|
||||
_debug Le_Listen_V4 "$Le_Listen_V4"
|
||||
_debug Le_Listen_V6 "$Le_Listen_V6"
|
||||
|
||||
_NC="socat"
|
||||
if [ "$Le_Listen_V6" ]; then
|
||||
_NC="$_NC -6"
|
||||
else
|
||||
_NC="$_NC -4"
|
||||
fi
|
||||
if _exists "socat"; then
|
||||
_NC="socat"
|
||||
if [ "$Le_Listen_V6" ]; then
|
||||
_NC="$_NC -6"
|
||||
SOCAT_OPTIONS=TCP6-LISTEN
|
||||
elif [ "$Le_Listen_V4" ]; then
|
||||
_NC="$_NC -4"
|
||||
SOCAT_OPTIONS=TCP4-LISTEN
|
||||
else
|
||||
SOCAT_OPTIONS=TCP-LISTEN
|
||||
fi
|
||||
|
||||
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
|
||||
_NC="$_NC -d -d -v"
|
||||
fi
|
||||
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
|
||||
_NC="$_NC -d -d -v"
|
||||
fi
|
||||
|
||||
SOCAT_OPTIONS=TCP-LISTEN:$Le_HTTPPort,crlf,reuseaddr,fork
|
||||
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
|
||||
|
||||
#Adding bind to local-address
|
||||
if [ "$ncaddr" ]; then
|
||||
SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}"
|
||||
fi
|
||||
#Adding bind to local-address
|
||||
if [ "$ncaddr" ]; then
|
||||
SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}"
|
||||
fi
|
||||
|
||||
_content_len="$(printf "%s" "$content" | wc -c)"
|
||||
_debug _content_len "$_content_len"
|
||||
_debug "_NC" "$_NC $SOCAT_OPTIONS"
|
||||
export _SOCAT_ERR="$(_mktemp)"
|
||||
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
|
||||
_content_len="$(printf "%s" "$content" | wc -c)"
|
||||
_debug _content_len "$_content_len"
|
||||
_debug "_NC" "$_NC $SOCAT_OPTIONS"
|
||||
export _SOCAT_ERR="$(_mktemp)"
|
||||
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
|
||||
echo 'HTTP/1.0 200 OK'; \
|
||||
echo 'Content-Length\: $_content_len'; \
|
||||
echo ''; \
|
||||
printf '%s' '$content';" 2>"$_SOCAT_ERR" &
|
||||
serverproc="$!"
|
||||
serverproc="$!"
|
||||
else
|
||||
_PYTHON=""
|
||||
if _exists "python3"; then
|
||||
_PYTHON="python3"
|
||||
elif _exists "python2"; then
|
||||
_PYTHON="python2"
|
||||
elif _exists "python"; then
|
||||
_PYTHON="python"
|
||||
fi
|
||||
if [ "$_PYTHON" ]; then
|
||||
_debug "Using python: $_PYTHON"
|
||||
_AF="socket.AF_INET"
|
||||
_BIND_ADDR="0.0.0.0"
|
||||
if [ "$Le_Listen_V6" ]; then
|
||||
_AF="socket.AF_INET6"
|
||||
_BIND_ADDR="::"
|
||||
fi
|
||||
if [ "$ncaddr" ]; then
|
||||
_BIND_ADDR="$ncaddr"
|
||||
fi
|
||||
export _SOCAT_ERR="$(_mktemp)"
|
||||
$_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3];
|
||||
while True:
|
||||
c,a=s.accept()
|
||||
c.sendall(res.encode() if hasattr(res, 'encode') else res)
|
||||
c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
|
||||
serverproc="$!"
|
||||
_NC="$_PYTHON"
|
||||
else
|
||||
_err "Please install socat or python first for standalone mode."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$_SOCAT_ERR" ]; then
|
||||
if grep "Permission denied" "$_SOCAT_ERR" >/dev/null; then
|
||||
_err "socat: $(cat $_SOCAT_ERR)"
|
||||
_err "$_NC: $(cat $_SOCAT_ERR)"
|
||||
_err "Can not listen for user: $(whoami)"
|
||||
_err "Maybe try with root again?"
|
||||
rm -f "$_SOCAT_ERR"
|
||||
@@ -2733,6 +2826,7 @@ _clearAPI() {
|
||||
ACME_REVOKE_CERT=""
|
||||
ACME_NEW_NONCE=""
|
||||
ACME_AGREEMENT=""
|
||||
ACME_RENEWAL_INFO=""
|
||||
}
|
||||
|
||||
#server
|
||||
@@ -2745,7 +2839,7 @@ _initAPI() {
|
||||
_request_retry_times=0
|
||||
while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do
|
||||
_request_retry_times=$(_math "$_request_retry_times" + 1)
|
||||
response=$(_get "$_api_server")
|
||||
response=$(_get "$_api_server" "" 10)
|
||||
if [ "$?" != "0" ]; then
|
||||
_debug2 "response" "$response"
|
||||
_info "Cannot init API for: $_api_server."
|
||||
@@ -2777,6 +2871,9 @@ _initAPI() {
|
||||
ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3)
|
||||
export ACME_AGREEMENT
|
||||
|
||||
ACME_RENEWAL_INFO=$(echo "$response" | _egrep_o 'renewalInfo" *: *"[^"]*"' | cut -d '"' -f 3)
|
||||
export ACME_RENEWAL_INFO
|
||||
|
||||
_debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE"
|
||||
_debug "ACME_NEW_AUTHZ" "$ACME_NEW_AUTHZ"
|
||||
_debug "ACME_NEW_ORDER" "$ACME_NEW_ORDER"
|
||||
@@ -2784,6 +2881,7 @@ _initAPI() {
|
||||
_debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT"
|
||||
_debug "ACME_AGREEMENT" "$ACME_AGREEMENT"
|
||||
_debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE"
|
||||
_debug "ACME_RENEWAL_INFO" "$ACME_RENEWAL_INFO"
|
||||
if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then
|
||||
return 0
|
||||
fi
|
||||
@@ -3491,7 +3589,7 @@ _on_before_issue() {
|
||||
_debug _chk_alt_domains "$_chk_alt_domains"
|
||||
#run pre hook
|
||||
if [ "$_chk_pre_hook" ]; then
|
||||
_info "Runing pre hook:'$_chk_pre_hook'"
|
||||
_info "Running pre hook:'$_chk_pre_hook'"
|
||||
if ! (
|
||||
export Le_Domain="$_chk_main_domain"
|
||||
export Le_Alt="$_chk_alt_domains"
|
||||
@@ -3502,9 +3600,9 @@ _on_before_issue() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if _hasfield "$_chk_web_roots" "$NO_VALUE"; then
|
||||
if ! _exists "socat"; then
|
||||
_err "Please install socat tools first."
|
||||
if _hasfield "$_chk_web_roots" "$NO_VALUE" && [ "$_chk_web_roots" = "$NO_VALUE" ]; then
|
||||
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
|
||||
_err "Please install socat or python tools first."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
@@ -4394,6 +4492,8 @@ issue() {
|
||||
_preferred_chain="${15}"
|
||||
_valid_from="${16}"
|
||||
_valid_to="${17}"
|
||||
_certificate_profile="${18}"
|
||||
_extended_key_usage="${19}"
|
||||
|
||||
if [ -z "$_ACME_IS_RENEW" ]; then
|
||||
_initpath "$_main_domain" "$_key_length"
|
||||
@@ -4413,7 +4513,7 @@ issue() {
|
||||
Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime)
|
||||
_debug Le_NextRenewTime "$Le_NextRenewTime"
|
||||
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
|
||||
_valid_to_saved=$(_readdomainconf Le_Valid_to)
|
||||
_valid_to_saved=$(_readdomainconf Le_Valid_To)
|
||||
if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then
|
||||
_info "The domain is set to be valid to: $_valid_to_saved"
|
||||
_info "It cannot be renewed automatically"
|
||||
@@ -4469,6 +4569,11 @@ issue() {
|
||||
else
|
||||
_cleardomainconf "Le_Preferred_Chain"
|
||||
fi
|
||||
if [ "$_certificate_profile" ]; then
|
||||
_savedomainconf "Le_Certificate_Profile" "$_certificate_profile"
|
||||
else
|
||||
_cleardomainconf "Le_Certificate_Profile"
|
||||
fi
|
||||
|
||||
Le_API="$ACME_DIRECTORY"
|
||||
_savedomainconf "Le_API" "$Le_API"
|
||||
@@ -4480,6 +4585,7 @@ issue() {
|
||||
|
||||
if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then
|
||||
_err "_on_before_issue."
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -4532,12 +4638,25 @@ issue() {
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then
|
||||
_keyusage="$_extended_key_usage"
|
||||
if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then
|
||||
if [ -z "$_keyusage" ]; then
|
||||
#https://github.com/acmesh-official/acme.sh/issues/6610
|
||||
#google accepts serverauth only
|
||||
_keyusage="serverAuth"
|
||||
fi
|
||||
fi
|
||||
if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then
|
||||
_err "Error creating CSR."
|
||||
_clearup
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
if [ "$_extended_key_usage" ]; then
|
||||
_savedomainconf "Le_ExtKeyUse" "$_extended_key_usage"
|
||||
else
|
||||
_cleardomainconf "Le_ExtKeyUse"
|
||||
fi
|
||||
fi
|
||||
|
||||
_savedomainconf "Le_Keylength" "$_key_length"
|
||||
@@ -4600,6 +4719,9 @@ issue() {
|
||||
if [ "$_notAfter" ]; then
|
||||
_newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\""
|
||||
fi
|
||||
if [ "$_certificate_profile" ]; then
|
||||
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
|
||||
fi
|
||||
_debug "STEP 1, Ordering a Certificate"
|
||||
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
|
||||
_err "Error creating new order."
|
||||
@@ -4739,7 +4861,8 @@ $_authorizations_map"
|
||||
_debug keyauthorization "$keyauthorization"
|
||||
fi
|
||||
|
||||
entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
|
||||
# Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018
|
||||
entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
|
||||
_debug entry "$entry"
|
||||
|
||||
if [ -z "$keyauthorization" -a -z "$entry" ]; then
|
||||
@@ -4989,9 +5112,11 @@ $_authorizations_map"
|
||||
|
||||
_debug "Writing token: $token to $wellknown_path/$token"
|
||||
|
||||
mkdir -p "$wellknown_path"
|
||||
|
||||
if ! printf "%s" "$keyauthorization" >"$wellknown_path/$token"; then
|
||||
# Ensure .well-known is visible to web server user/group
|
||||
# https://github.com/Neilpang/acme.sh/pull/32
|
||||
if ! (umask ugo+rx &&
|
||||
mkdir -p "$wellknown_path" &&
|
||||
printf "%s" "$keyauthorization" >"$wellknown_path/$token"); then
|
||||
_err "$d: Cannot write token to file: $wellknown_path/$token"
|
||||
_clearupwebbroot "$_currentRoot" "$removelevel" "$token"
|
||||
_clearup
|
||||
@@ -5111,6 +5236,19 @@ $_authorizations_map"
|
||||
_on_issue_err "$_post_hook" "$vlist"
|
||||
return 1
|
||||
fi
|
||||
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
||||
_sleep_overload_retry_sec=$_retryafter
|
||||
if [ "$_sleep_overload_retry_sec" ]; then
|
||||
if [ $_sleep_overload_retry_sec -le 600 ]; then
|
||||
_sleep $_sleep_overload_retry_sec
|
||||
else
|
||||
_info "The retryafter=$_retryafter value is too large (> 600), will not retry anymore."
|
||||
_clearupwebbroot "$_currentRoot" "$removelevel" "$token"
|
||||
_clearup
|
||||
_on_issue_err "$_post_hook" "$vlist"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
done
|
||||
@@ -5152,6 +5290,16 @@ $_authorizations_map"
|
||||
return 1
|
||||
fi
|
||||
break
|
||||
elif _contains "$response" "\"ready\""; then
|
||||
_info "Order status is 'ready', let's sleep and retry."
|
||||
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
||||
_debug "_retryafter" "$_retryafter"
|
||||
if [ "$_retryafter" ]; then
|
||||
_info "Sleeping for $_retryafter seconds then retrying"
|
||||
_sleep $_retryafter
|
||||
else
|
||||
_sleep 2
|
||||
fi
|
||||
elif _contains "$response" "\"processing\""; then
|
||||
_info "Order status is 'processing', let's sleep and retry."
|
||||
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
||||
@@ -5350,10 +5498,10 @@ $_authorizations_map"
|
||||
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
|
||||
|
||||
#convert to pkcs12
|
||||
Le_PFXPassword="$(_readdomainconf Le_PFXPassword)"
|
||||
if [ "$Le_PFXPassword" ]; then
|
||||
_toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword"
|
||||
fi
|
||||
export CERT_PFX_PATH
|
||||
|
||||
if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
|
||||
_savedomainconf "Le_RealCertPath" "$_real_cert"
|
||||
@@ -5421,10 +5569,6 @@ renew() {
|
||||
_info "Switching back to $CA_LETSENCRYPT_V2"
|
||||
Le_API="$CA_LETSENCRYPT_V2"
|
||||
;;
|
||||
"$CA_BUYPASS_TEST")
|
||||
_info "Switching back to $CA_BUYPASS"
|
||||
Le_API="$CA_BUYPASS"
|
||||
;;
|
||||
"$CA_GOOGLE_TEST")
|
||||
_info "Switching back to $CA_GOOGLE"
|
||||
Le_API="$CA_GOOGLE"
|
||||
@@ -5466,6 +5610,11 @@ renew() {
|
||||
Le_PostHook="$(_readdomainconf Le_PostHook)"
|
||||
Le_RenewHook="$(_readdomainconf Le_RenewHook)"
|
||||
Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)"
|
||||
Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)"
|
||||
Le_Valid_From="$(_readdomainconf Le_Valid_From)"
|
||||
Le_Valid_To="$(_readdomainconf Le_Valid_To)"
|
||||
Le_ExtKeyUse="$(_readdomainconf Le_ExtKeyUse)"
|
||||
|
||||
# When renewing from an old version, the empty Le_Keylength means 2048.
|
||||
# Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over
|
||||
# time but an empty value implies 2048 specifically.
|
||||
@@ -5473,7 +5622,14 @@ renew() {
|
||||
if [ -z "$Le_Keylength" ]; then
|
||||
Le_Keylength=2048
|
||||
fi
|
||||
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To"
|
||||
if [ "$CA_LETSENCRYPT_V2" = "$Le_API" ]; then
|
||||
#letsencrypt doesn't support ocsp anymore
|
||||
if [ "$Le_OCSP_Staple" ]; then
|
||||
export Le_OCSP_Staple=""
|
||||
_cleardomainconf Le_OCSP_Staple
|
||||
fi
|
||||
fi
|
||||
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse"
|
||||
res="$?"
|
||||
if [ "$res" != "0" ]; then
|
||||
return "$res"
|
||||
@@ -5640,6 +5796,10 @@ signcsr() {
|
||||
_local_addr="${11}"
|
||||
_challenge_alias="${12}"
|
||||
_preferred_chain="${13}"
|
||||
_valid_f="${14}"
|
||||
_valid_t="${15}"
|
||||
_cert_prof="${16}"
|
||||
_en_key_usage="${17}"
|
||||
|
||||
_csrsubj=$(_readSubjectFromCSR "$_csrfile")
|
||||
if [ "$?" != "0" ]; then
|
||||
@@ -5683,7 +5843,7 @@ signcsr() {
|
||||
_info "Copying CSR to: $CSR_PATH"
|
||||
cp "$_csrfile" "$CSR_PATH"
|
||||
|
||||
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain"
|
||||
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" "$_valid_f" "$_valid_t" "$_cert_prof" "$_en_key_usage"
|
||||
|
||||
}
|
||||
|
||||
@@ -5734,9 +5894,10 @@ list() {
|
||||
_sep="|"
|
||||
if [ "$_raw" ]; then
|
||||
if [ -z "$_domain" ]; then
|
||||
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}CA${_sep}Created${_sep}Renew"
|
||||
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew"
|
||||
fi
|
||||
for di in "${CERT_HOME}"/*.*/; do
|
||||
for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do
|
||||
[ -d "$di" ] || continue
|
||||
d=$(basename "$di")
|
||||
_debug d "$d"
|
||||
(
|
||||
@@ -5749,7 +5910,7 @@ list() {
|
||||
. "$DOMAIN_CONF"
|
||||
_ca="$(_getCAShortName "$Le_API")"
|
||||
if [ -z "$_domain" ]; then
|
||||
printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr"
|
||||
printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_Certificate_Profile${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr"
|
||||
else
|
||||
if [ "$_domain" = "$d" ]; then
|
||||
cat "$DOMAIN_CONF"
|
||||
@@ -5768,6 +5929,48 @@ list() {
|
||||
|
||||
}
|
||||
|
||||
list_profiles() {
|
||||
_initpath
|
||||
_initAPI
|
||||
|
||||
_l_server_url="$ACME_DIRECTORY"
|
||||
_l_server_name="$(_getCAShortName "$_l_server_url")"
|
||||
_info "Fetching profiles from $_l_server_name ($_l_server_url)..."
|
||||
|
||||
response=$(_get "$_l_server_url" "" 10)
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Failed to connect to CA directory: $_l_server_url"
|
||||
return 1
|
||||
fi
|
||||
|
||||
normalized_response=$(echo "$response" | _normalizeJson)
|
||||
profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *\{[^\}]*\}')
|
||||
|
||||
if [ -z "$profiles_json" ]; then
|
||||
_info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Strip the outer layer to get the key-value pairs
|
||||
profiles_kv=$(echo "$profiles_json" | sed 's/"profiles" *: *{//' | sed 's/}$//' | tr ',' '\n')
|
||||
|
||||
printf "\n%-15s %s\n" "name" "info"
|
||||
printf -- "--------------------------------------------------------------------\n"
|
||||
|
||||
_old_IFS="$IFS"
|
||||
IFS='
|
||||
'
|
||||
for pair in $profiles_kv; do
|
||||
# Trim quotes and whitespace
|
||||
_name=$(echo "$pair" | cut -d: -f1 | tr -d '" \t')
|
||||
_info_url=$(echo "$pair" | cut -d: -f2- | sed 's/^ *//' | tr -d '"')
|
||||
printf "%-15s %s\n" "$_name" "$_info_url"
|
||||
done
|
||||
IFS="$_old_IFS"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_deploy() {
|
||||
_d="$1"
|
||||
_hooks="$2"
|
||||
@@ -5792,7 +5995,7 @@ _deploy() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then
|
||||
if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CERT_PFX_PATH"; then
|
||||
_err "Error deploying for domain: $_d"
|
||||
return 1
|
||||
fi
|
||||
@@ -5955,7 +6158,7 @@ _installcert() {
|
||||
); then
|
||||
_info "$(__green "Reload successful")"
|
||||
else
|
||||
_err "Reload error for: $Le_Domain"
|
||||
_err "Reload error for: $_main_domain"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -6035,7 +6238,7 @@ installcronjob() {
|
||||
_script="$(_readlink "$_SCRIPT_")"
|
||||
_debug _script "$_script"
|
||||
if [ -f "$_script" ]; then
|
||||
_info "Usinging the current script from: $_script"
|
||||
_info "Using the current script from: $_script"
|
||||
lesh="$_script"
|
||||
else
|
||||
_err "Cannot install cronjob, $PROJECT_ENTRY not found."
|
||||
@@ -6306,7 +6509,8 @@ _deactivate() {
|
||||
fi
|
||||
_debug "Trigger validation."
|
||||
vtype="$(_getIdType "$_d_domain")"
|
||||
entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
|
||||
# Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018
|
||||
entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
|
||||
_debug entry "$entry"
|
||||
if [ -z "$entry" ]; then
|
||||
_err "$d: Cannot get domain token"
|
||||
@@ -6390,6 +6594,36 @@ deactivate() {
|
||||
done
|
||||
}
|
||||
|
||||
#cert
|
||||
_getAKI() {
|
||||
_cert="$1"
|
||||
openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :'
|
||||
}
|
||||
|
||||
#cert
|
||||
_getSerial() {
|
||||
_cert="$1"
|
||||
openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2
|
||||
}
|
||||
|
||||
#cert
|
||||
_get_ARI() {
|
||||
_cert="$1"
|
||||
_aki=$(_getAKI "$_cert")
|
||||
_ser=$(_getSerial "$_cert")
|
||||
_debug2 "_aki" "$_aki"
|
||||
_debug2 "_ser" "$_ser"
|
||||
|
||||
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
|
||||
_debug2 "_akiurl" "$_akiurl"
|
||||
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
|
||||
_debug2 "_serurl" "$_serurl"
|
||||
|
||||
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
|
||||
_get "$_ARI_URL"
|
||||
|
||||
}
|
||||
|
||||
# Detect profile file if not specified as environment variable
|
||||
_detect_profile() {
|
||||
if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
|
||||
@@ -6438,6 +6672,7 @@ _initconf() {
|
||||
#NO_TIMESTAMP=1
|
||||
|
||||
" >"$ACCOUNT_CONF_PATH"
|
||||
chmod 600 "$ACCOUNT_CONF_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -6473,9 +6708,9 @@ _precheck() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _exists "socat"; then
|
||||
_err "It is recommended to install socat first."
|
||||
_err "We use socat for the standalone server, which is used for standalone mode."
|
||||
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
|
||||
_err "It is recommended to install socat or python first."
|
||||
_err "We use socat or python for the standalone server, which is used for standalone mode."
|
||||
_err "If you don't want to use standalone mode, you may ignore this warning."
|
||||
fi
|
||||
|
||||
@@ -6787,7 +7022,7 @@ _send_notify() {
|
||||
|
||||
_nsource="$NOTIFY_SOURCE"
|
||||
if [ -z "$_nsource" ]; then
|
||||
_nsource="$(hostname)"
|
||||
_nsource="$(uname -n)"
|
||||
fi
|
||||
|
||||
_nsubject="$_nsubject by $_nsource"
|
||||
@@ -6945,6 +7180,9 @@ Parameters:
|
||||
If no match, the default offered chain will be used. (default: empty)
|
||||
See: $_PREFERRED_CHAIN_WIKI
|
||||
|
||||
--cert-profile, --certificate-profile <profile> If the CA offers profiles, select the desired profile
|
||||
See: $_PROFILESELECTION_WIKI
|
||||
|
||||
--valid-to <date-time> Request the NotAfter field of the cert.
|
||||
See: $_VALIDITY_WIKI
|
||||
--valid-from <date-time> Request the NotBefore field of the cert.
|
||||
@@ -6989,7 +7227,7 @@ Parameters:
|
||||
|
||||
--accountconf <file> Specifies a customized account config file.
|
||||
--home <directory> Specifies the home dir for $PROJECT_NAME.
|
||||
--cert-home <directory> Specifies the home dir to save all the certs, only valid for '--install' command.
|
||||
--cert-home <directory> Specifies the home dir to save all the certs.
|
||||
--config-home <directory> Specifies the home dir to save all the configurations.
|
||||
--useragent <string> Specifies the user agent string. it will be saved for future use too.
|
||||
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
|
||||
@@ -7021,6 +7259,8 @@ Parameters:
|
||||
--auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted.
|
||||
--listen-v4 Force standalone/tls server to listen at ipv4.
|
||||
--listen-v6 Force standalone/tls server to listen at ipv6.
|
||||
--request-v4 Force client requests to use ipv4 to connect to the CA server.
|
||||
--request-v6 Force client requests to use ipv6 to connect to the CA server.
|
||||
--openssl-bin <file> Specifies a custom openssl bin location.
|
||||
--use-wget Force to use wget, if you have both curl and wget installed.
|
||||
--yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode.
|
||||
@@ -7139,6 +7379,24 @@ _processAccountConf() {
|
||||
_saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET"
|
||||
fi
|
||||
|
||||
if [ "$_request_v6" ]; then
|
||||
_saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6"
|
||||
_clearaccountconf "ACME_USE_IPV4_REQUESTS"
|
||||
ACME_USE_IPV4_REQUESTS=
|
||||
elif [ "$_request_v4" ]; then
|
||||
_saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4"
|
||||
_clearaccountconf "ACME_USE_IPV6_REQUESTS"
|
||||
ACME_USE_IPV6_REQUESTS=
|
||||
elif [ "$ACME_USE_IPV6_REQUESTS" ]; then
|
||||
_saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS"
|
||||
_clearaccountconf "ACME_USE_IPV4_REQUESTS"
|
||||
ACME_USE_IPV4_REQUESTS=
|
||||
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
|
||||
_saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS"
|
||||
_clearaccountconf "ACME_USE_IPV6_REQUESTS"
|
||||
ACME_USE_IPV6_REQUESTS=
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
_checkSudo() {
|
||||
@@ -7304,6 +7562,8 @@ _process() {
|
||||
_local_address=""
|
||||
_log_level=""
|
||||
_auto_upgrade=""
|
||||
_request_v4=""
|
||||
_request_v6=""
|
||||
_listen_v4=""
|
||||
_listen_v6=""
|
||||
_openssl_bin=""
|
||||
@@ -7320,6 +7580,8 @@ _process() {
|
||||
_preferred_chain=""
|
||||
_valid_from=""
|
||||
_valid_to=""
|
||||
_certificate_profile=""
|
||||
_extended_key_usage=""
|
||||
while [ ${#} -gt 0 ]; do
|
||||
case "${1}" in
|
||||
|
||||
@@ -7423,6 +7685,9 @@ _process() {
|
||||
--set-default-chain)
|
||||
_CMD="setdefaultchain"
|
||||
;;
|
||||
--list-profiles)
|
||||
_CMD="list_profiles"
|
||||
;;
|
||||
-d | --domain)
|
||||
_dvalue="$2"
|
||||
|
||||
@@ -7638,6 +7903,10 @@ _process() {
|
||||
_valid_to="$2"
|
||||
shift
|
||||
;;
|
||||
--certificate-profile | --cert-profile)
|
||||
_certificate_profile="$2"
|
||||
shift
|
||||
;;
|
||||
--httpport)
|
||||
_httpport="$2"
|
||||
Le_HTTPPort="$_httpport"
|
||||
@@ -7708,7 +7977,7 @@ _process() {
|
||||
shift
|
||||
;;
|
||||
--extended-key-usage)
|
||||
Le_ExtKeyUse="$2"
|
||||
_extended_key_usage="$2"
|
||||
shift
|
||||
;;
|
||||
--ocsp-must-staple | --ocsp)
|
||||
@@ -7761,6 +8030,18 @@ _process() {
|
||||
fi
|
||||
AUTO_UPGRADE="$_auto_upgrade"
|
||||
;;
|
||||
--request-v4)
|
||||
_request_v4="1"
|
||||
ACME_USE_IPV4_REQUESTS="1"
|
||||
_request_v6=""
|
||||
ACME_USE_IPV6_REQUESTS=""
|
||||
;;
|
||||
--request-v6)
|
||||
_request_v6="1"
|
||||
ACME_USE_IPV6_REQUESTS="1"
|
||||
_request_v4=""
|
||||
ACME_USE_IPV4_REQUESTS=""
|
||||
;;
|
||||
--listen-v4)
|
||||
_listen_v4="1"
|
||||
Le_Listen_V4="$_listen_v4"
|
||||
@@ -7913,13 +8194,13 @@ _process() {
|
||||
uninstall) uninstall "$_nocron" ;;
|
||||
upgrade) upgrade ;;
|
||||
issue)
|
||||
issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to"
|
||||
issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
|
||||
;;
|
||||
deploy)
|
||||
deploy "$_domain" "$_deploy_hook" "$_ecc"
|
||||
;;
|
||||
signcsr)
|
||||
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain"
|
||||
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
|
||||
;;
|
||||
showcsr)
|
||||
showcsr "$_csr" "$_domain"
|
||||
@@ -7984,6 +8265,9 @@ _process() {
|
||||
setdefaultchain)
|
||||
setdefaultchain "$_preferred_chain"
|
||||
;;
|
||||
list_profiles)
|
||||
list_profiles
|
||||
;;
|
||||
*)
|
||||
if [ "$_CMD" ]; then
|
||||
_err "Invalid command: $_CMD"
|
||||
|
||||
955
.templates/bin/ssl-cert-check
Executable file
955
.templates/bin/ssl-cert-check
Executable file
@@ -0,0 +1,955 @@
|
||||
#!/usr/bin/env bash
|
||||
PROGRAMVERSION=4.14
|
||||
#
|
||||
# Program: SSL Certificate Check <ssl-cert-check>
|
||||
#
|
||||
# Source code home: https://github.com/Matty9191/ssl-cert-check
|
||||
#
|
||||
# Documentation: http://prefetch.net/articles/checkcertificate.html
|
||||
#
|
||||
# Author: Matty < matty at prefetch dot net >
|
||||
#
|
||||
# Last Updated: 11-12-2020
|
||||
#
|
||||
# Revision History:
|
||||
#
|
||||
# Version 4.14
|
||||
# - Fixed HOST / PORT discovery @mhow2
|
||||
#
|
||||
# Version 4.13
|
||||
# - Reverted the file checking logic which breaks $RETCODE
|
||||
#
|
||||
# Version 4.12
|
||||
# - Fixed various logic errors and typos -- Daniel Lewart
|
||||
#
|
||||
# Version 4.10
|
||||
# - Replace tabs with spaces
|
||||
# - More shllcheck cleanup work
|
||||
# - Remove unused DEBUG variable
|
||||
# - Fixed an innocuous whitespace bug in TLSFLAG variable creation
|
||||
# - Set the default TLS version to 1.1 (can be overridden with -v)
|
||||
# - Switched openssl CLI options to use an array. The reasons why
|
||||
# are documented here: http://mywiki.wooledge.org/BashFAQ/050
|
||||
#
|
||||
# Version 4.9
|
||||
# - Add a signal handler to call the cleanup funtion
|
||||
# if the script doesn't exit() cleanly -- Timothe Litt
|
||||
#
|
||||
# Version 4.8
|
||||
# - More mail client fixes
|
||||
#
|
||||
# Version 4.7
|
||||
# - Revert SENDER to ""
|
||||
# - More shellcheck cleanup
|
||||
#
|
||||
# Version 4.6
|
||||
# - Fixed programming logic error
|
||||
#
|
||||
# Version 4.5
|
||||
# - Re-work mailx support for FreeBSD
|
||||
# - More shellcheck fixes
|
||||
#
|
||||
# Version 4.4
|
||||
# - Use command -v instead of which utility to satisfy shellcheck.
|
||||
# - Fix unquoted MAIL and MAILMODE variables in help output
|
||||
# - More shellcheck fixes
|
||||
#
|
||||
# Version 4.3
|
||||
# - Fixed a typo in the program version
|
||||
#
|
||||
# Version 4.2
|
||||
# - Change CERTDAYS to CERTDIFF in the e-mail subject.
|
||||
#
|
||||
# Version 4.1
|
||||
# - Fix usage output
|
||||
#
|
||||
# Version 4.0
|
||||
# - Updated the script syntax to align with UNIX shell programming
|
||||
# - Check for DNS resolution failures
|
||||
# - First round of updates to make shellcheck happy
|
||||
# - Rework the logic to call mailx.
|
||||
# - Print the version with the "-V" option.
|
||||
# - Define the version in the PROGRAMVERSION variable
|
||||
#
|
||||
# Version 3.31
|
||||
# - Fixed the test for the -servername flag -- Kitson Consulting.
|
||||
#
|
||||
# Version 3.30
|
||||
# - Use highest returncode for Nagios output -- Marcel Pennewiss
|
||||
# - Set RETCODE to 3 (unknown) if a certificate file does not exist -- Marcel Pennewiss
|
||||
# - Add a "-d" option to specify a directory or file mask pattern -- Marcel Pennewiss
|
||||
# - Add a "-N" option to create summarized Nagios output -- Marcel Pennewiss
|
||||
# - Cleaned up many formatting -- Marcel Pennewiss
|
||||
#
|
||||
# Versione 3.29a
|
||||
# - Added option to specify email sender address
|
||||
#
|
||||
# Version 3.29
|
||||
# - Add the openssl -servername flag if it shows up in help.
|
||||
#
|
||||
# Version 3.28
|
||||
# - Added a DEBUG option to assist with debugging folks who use the script
|
||||
#
|
||||
# Version 3.27
|
||||
# - Allow white spaces to exist in the certificate file list
|
||||
# - Add an additional check to pick up bad / non-existent certificates
|
||||
# - Add a check to look for the existence of a mail program. Error out if it's not present.
|
||||
# - Enable the TLS -servername extension by default - Juergen Knaack & Johan Denoyer
|
||||
#
|
||||
# Version 3.26
|
||||
# - Allow the certificate type (PEM, DER, NET) to be passed on the command line
|
||||
#
|
||||
# Version 3.25
|
||||
# - Check for "no route to host" errors -- Dan Doyle
|
||||
# - Set RETCODE to 3 (unknown) if a connection error occurs -- Dan Doyle
|
||||
# - Documentation fixes
|
||||
#
|
||||
# Version 3.24
|
||||
# - Utilize the -clcerts option to limit the results to client certificates - Eitan Katznelson
|
||||
#
|
||||
# Version 3.23
|
||||
# - Fixed typo in date2julian routine -- Ken Cook
|
||||
#
|
||||
# Version 3.22
|
||||
# - Change the validation option to "-V"
|
||||
# - Add a "-v" option to specify a specific protocol version (ssl2, ssl3 or tls)
|
||||
#
|
||||
# Version 3.21
|
||||
# - Adjust e-mail checking to avoid exiting if notifications aren't enabled -- Nick Anderson
|
||||
# - Added the number of days until expiration to the Nagios output -- Nick Anderson
|
||||
#
|
||||
# Version 3.20
|
||||
# - Fixed a bug in certificate length checking -- Tim Nowaczyk
|
||||
#
|
||||
# Version 3.19
|
||||
# - Added check to verify the certificate retrieved is valid
|
||||
#
|
||||
# Version 3.18
|
||||
# - Add support for connecting to FTP servers -- Paul A Sand
|
||||
#
|
||||
# Version 3.17
|
||||
# - Add support for connecting to imap servers -- Joerg Pareigis
|
||||
#
|
||||
# Version 3.16
|
||||
# - Add support for connecting to the mail sbmission port -- Luis E. Munoz
|
||||
#
|
||||
# Version 3.15
|
||||
# - Adjusted the file checking logic to use the correct certificate -- Maciej Szudejko
|
||||
# - Add sbin to the default search paths for OpenBSD compatibility -- Alex Popov
|
||||
# - Use cut instead of substring processing to ensure compatibility -- Alex Popov
|
||||
#
|
||||
# Version 3.14
|
||||
# - Fixed the Common Name parser to handle DN's where the CN is not the last item
|
||||
# eg. EmailAddr -- Jason Brothers
|
||||
# - Added the ability to grab the serial number -- Jason Brothers
|
||||
# - Added the "-b" option to print results without a header -- Jason Brothers
|
||||
# - Added the "-v" option for certificate validation -- Jason Brothers
|
||||
#
|
||||
# Version 3.13
|
||||
# - Updated the subject line to include the hostname as well as
|
||||
# the common name embedded in the X509 certificate (if it's
|
||||
# available) -- idea proposed by Mike Burns
|
||||
#
|
||||
# Version 3.12
|
||||
# - Updated the license to allow redistribution and modification
|
||||
#
|
||||
# Version 3.11
|
||||
# - Added ability to comment out lines in files passed
|
||||
# to the "-f" option -- Brett Stauner
|
||||
# - Fixed comment next to file processing logic
|
||||
#
|
||||
# Version 3.10
|
||||
# - Fixed POP3 port -- Simon Matter
|
||||
#
|
||||
# Version 3.9
|
||||
# - Switched binary location logic to use which utility
|
||||
#
|
||||
# Version 3.8
|
||||
# - Fixed display on 80 column displays
|
||||
# - Cleaned up the formatting
|
||||
#
|
||||
# Version 3.7
|
||||
# - Fixed bug in NAGIOS tests -- Ben Allen
|
||||
#
|
||||
# Version 3.6
|
||||
# - Added support for certificates stored in PKCS#12 databases -- Ken Gallo
|
||||
# - Cleaned up comments
|
||||
# - Adjusted variables to be more consistent
|
||||
#
|
||||
# Version 3.5
|
||||
# - Added support for NAGIOS -- Quanah Gibson-Mount
|
||||
# - Added additional checks for mail -- Quanah Gibson-Mount
|
||||
# - Convert tabs to spaces -- Quanah Gibson-Mount
|
||||
# - Cleaned up usage() routine
|
||||
# - Added additional checks for openssl
|
||||
#
|
||||
# Version 3.4
|
||||
# - Added a missing "{" to line 364 -- Ken Gallo
|
||||
# - Move mktemp to the start of the main body to avoid errors
|
||||
# - Adjusted default binary paths to make sure the script just works
|
||||
# w/ Solaris, BSD and Linux hosts
|
||||
#
|
||||
# Version 3.3
|
||||
# - Added common name from X.509 certificate file to E-mail body / header -- Doug Curtis
|
||||
# - Fixed several documentation errors
|
||||
# - Use mktemp to create temporary files
|
||||
# - Convert printf, sed and awk to variables
|
||||
# - Check for printf, sed, awk and mktemp binaries
|
||||
# - Add additional logic to make sure mktemp returned a valid temporary file
|
||||
#
|
||||
# Version 3.2
|
||||
# - Added option to list certificates in the file passed to "-f".
|
||||
#
|
||||
# Version 3.1
|
||||
# - Added handling for starttls for smtp -- Marco Amrein
|
||||
# - Added handling for starttls for pop3 (without s) -- Marco Amrein
|
||||
# - Removed extra spacing at end of script
|
||||
#
|
||||
# Version 3.0
|
||||
# - Added "-i" option to print certificate issuer
|
||||
# - Removed $0 from Subject line of outbound e-mails
|
||||
# - Fixed some typographical errors
|
||||
# - Removed redundant "-b" option
|
||||
#
|
||||
# Version 2.0
|
||||
# - Fixed an issue with e-mails formatting incorrectly
|
||||
# - Added additional space to host column -- Darren-Perot Spruell
|
||||
# - Replaced GNU date dependency with CHRIS F. A. JOHNSON's
|
||||
# date2julian shell function. This routine can be found on
|
||||
# page 170 of Chris's book "Shell Scripting Recipes: A
|
||||
# Problem-Solution Approach," ISBN #1590594711. Julian function
|
||||
# was created based on a post to comp.unix.shell by Tapani Tarvainen.
|
||||
# - Cleaned up function descriptions
|
||||
# - Removed several lines of redundant code
|
||||
# - Adjusted the help message
|
||||
#
|
||||
# Version 1.1
|
||||
# - Added "-c" flag to report expiration status of a PEM encoded
|
||||
# certificate -- Hampus Lundqvist
|
||||
# - Updated the prints messages to display the reason a connection
|
||||
# failed (connection refused, connection timeout, bad cert, etc)
|
||||
# - Updated the GNU date checking routines
|
||||
# - Added checks for each binary required
|
||||
# - Added checks for connection timeouts
|
||||
# - Added checks for GNU date
|
||||
# - Added a "-h" option
|
||||
# - Cleaned up the documentation
|
||||
#
|
||||
# Version 1.0
|
||||
# Initial Release
|
||||
#
|
||||
# Purpose:
|
||||
# ssl-cert-check checks to see if a digital certificate in X.509 format
|
||||
# has expired. ssl-cert-check can be run in interactive and batch mode,
|
||||
# and provides facilities to alarm if a certificate is about to expire.
|
||||
#
|
||||
# License:
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# Requirements:
|
||||
# Requires openssl
|
||||
#
|
||||
# Installation:
|
||||
# Copy the shell script to a suitable location
|
||||
#
|
||||
# Tested platforms:
|
||||
# -- Solaris 9 using /bin/bash
|
||||
# -- Solaris 10 using /bin/bash
|
||||
# -- OS X 10.4.2 using /bin/bash
|
||||
# -- OpenBSD using /bin/sh
|
||||
# -- FreeBSD using /bin/sh
|
||||
# -- Centos Linux 3, 4, 5 & 6 using /bin/bash
|
||||
# -- Redhat Enterprise Linux 3, 4, 5 & 6 using /bin/bash
|
||||
# -- Gentoo using /bin/bash
|
||||
#
|
||||
# Usage:
|
||||
# Refer to the usage() sub-routine, or invoke ssl-cert-check
|
||||
# with the "-h" option.
|
||||
#
|
||||
# Examples:
|
||||
# Please refer to the following site for documentation and examples:
|
||||
# http://prefetch.net/articles/checkcertificate.html
|
||||
|
||||
# Cleanup temp files if they exist
|
||||
trap cleanup EXIT INT TERM QUIT
|
||||
|
||||
PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/ssl/bin:/usr/sfw/bin
|
||||
export PATH
|
||||
|
||||
# Who to page when an expired certificate is detected (cmdline: -e)
|
||||
ADMIN="root"
|
||||
|
||||
# Email sender address for alarm notifications
|
||||
SENDER=""
|
||||
|
||||
# Number of days in the warning threshhold (cmdline: -x)
|
||||
WARNDAYS=30
|
||||
|
||||
# If QUIET is set to TRUE, don't print anything on the console (cmdline: -q)
|
||||
QUIET="FALSE"
|
||||
|
||||
# Don't send E-mail by default (cmdline: -a)
|
||||
ALARM="FALSE"
|
||||
|
||||
# Don't run as a Nagios plugin by default (cmdline: -n)
|
||||
NAGIOS="FALSE"
|
||||
|
||||
# Don't summarize Nagios output by default (cmdline: -N)
|
||||
NAGIOSSUMMARY="FALSE"
|
||||
|
||||
# NULL out the PKCSDBPASSWD variable for later use (cmdline: -k)
|
||||
PKCSDBPASSWD=""
|
||||
|
||||
# Type of certificate (PEM, DER, NET) (cmdline: -t)
|
||||
CERTTYPE="pem"
|
||||
|
||||
# Location of system binaries
|
||||
AWK=$(command -v awk)
|
||||
DATE=$(command -v date)
|
||||
GREP=$(command -v grep)
|
||||
OPENSSL=$(command -v openssl)
|
||||
PRINTF=$(command -v printf)
|
||||
SED=$(command -v sed)
|
||||
MKTEMP=$(command -v mktemp)
|
||||
FIND=$(command -v find)
|
||||
|
||||
# Try to find a mail client
|
||||
if [ -f /usr/bin/mailx ]; then
|
||||
MAIL="/usr/bin/mailx"
|
||||
MAILMODE="mailx"
|
||||
elif [ -f /bin/mail ]; then
|
||||
MAIL="/bin/mail"
|
||||
MAILMODE="mail"
|
||||
elif [ -f /usr/bin/mail ]; then
|
||||
MAIL="/usr/bin/mail"
|
||||
MAILMODE="mail"
|
||||
elif [ -f /sbin/mail ]; then
|
||||
MAIL="/sbin/mail"
|
||||
MAILMODE="mail"
|
||||
elif [ -f /usr/sbin/mail ]; then
|
||||
MAIL="/usr/sbin/mail"
|
||||
MAILMODE="mail"
|
||||
elif [ -f /usr/sbin/sendmail ]; then
|
||||
MAIL="/usr/sbin/sendmail"
|
||||
MAILMODE="sendmail"
|
||||
else
|
||||
MAIL="cantfindit"
|
||||
MAILMODE="cantfindit"
|
||||
fi
|
||||
|
||||
# Return code used by nagios. Initialize to 0.
|
||||
RETCODE=0
|
||||
|
||||
# Certificate counters and minimum difference. Initialize to 0.
|
||||
SUMMARY_VALID=0
|
||||
SUMMARY_WILL_EXPIRE=0
|
||||
SUMMARY_EXPIRED=0
|
||||
SUMMARY_MIN_DIFF=0
|
||||
SUMMARY_MIN_DATE=
|
||||
SUMMARY_MIN_HOST=
|
||||
SUMMARY_MIN_PORT=
|
||||
|
||||
# Set the default umask to be somewhat restrictive
|
||||
umask 077
|
||||
|
||||
|
||||
#####################################################
|
||||
# Purpose: Remove temporary files if the script doesn't
|
||||
# exit() cleanly
|
||||
#####################################################
|
||||
cleanup() {
|
||||
if [ -f "${CERT_TMP}" ]; then
|
||||
rm -f "${CERT_TMP}"
|
||||
fi
|
||||
|
||||
if [ -f "${ERROR_TMP}" ]; then
|
||||
rm -f "${ERROR_TMP}"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#####################################################
|
||||
### Send email
|
||||
### Accepts three parameters:
|
||||
### $1 -> sender email address
|
||||
### $2 -> email to send mail
|
||||
### $3 -> Subject
|
||||
### $4 -> Message
|
||||
#####################################################
|
||||
send_mail() {
|
||||
|
||||
FROM="${1}"
|
||||
TO="${2}"
|
||||
SUBJECT="${3}"
|
||||
MSG="${4}"
|
||||
|
||||
case "${MAILMODE}" in
|
||||
"mail")
|
||||
echo "$MSG" | "${MAIL}" -r "$FROM" -s "$SUBJECT" "$TO"
|
||||
;;
|
||||
"mailx")
|
||||
echo "$MSG" | "${MAIL}" -s "$SUBJECT" "$TO"
|
||||
;;
|
||||
"sendmail")
|
||||
(echo "Subject:$SUBJECT" && echo "TO:$TO" && echo "FROM:$FROM" && echo "$MSG") | "${MAIL}" "$TO"
|
||||
;;
|
||||
"*")
|
||||
echo "ERROR: You enabled automated alerts, but the mail binary could not be found."
|
||||
echo "FIX: Please modify the \${MAIL} and \${MAILMODE} variable in the program header."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
# Purpose: Convert a date from MONTH-DAY-YEAR to Julian format
|
||||
# Acknowledgements: Code was adapted from examples in the book
|
||||
# "Shell Scripting Recipes: A Problem-Solution Approach"
|
||||
# ( ISBN 1590594711 )
|
||||
# Arguments:
|
||||
# $1 -> Month (e.g., 06)
|
||||
# $2 -> Day (e.g., 08)
|
||||
# $3 -> Year (e.g., 2006)
|
||||
#############################################################################
|
||||
date2julian() {
|
||||
|
||||
if [ "${1}" != "" ] && [ "${2}" != "" ] && [ "${3}" != "" ]; then
|
||||
## Since leap years add aday at the end of February,
|
||||
## calculations are done from 1 March 0000 (a fictional year)
|
||||
d2j_tmpmonth=$((12 * $3 + $1 - 3))
|
||||
|
||||
## If it is not yet March, the year is changed to the previous year
|
||||
d2j_tmpyear=$(( d2j_tmpmonth / 12))
|
||||
|
||||
## The number of days from 1 March 0000 is calculated
|
||||
## and the number of days from 1 Jan. 4713BC is added
|
||||
echo $(( (734 * d2j_tmpmonth + 15) / 24
|
||||
- 2 * d2j_tmpyear + d2j_tmpyear/4
|
||||
- d2j_tmpyear/100 + d2j_tmpyear/400 + $2 + 1721119 ))
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
# Purpose: Convert a string month into an integer representation
|
||||
# Arguments:
|
||||
# $1 -> Month name (e.g., Sep)
|
||||
#############################################################################
|
||||
getmonth()
|
||||
{
|
||||
case ${1} in
|
||||
Jan) echo 1 ;;
|
||||
Feb) echo 2 ;;
|
||||
Mar) echo 3 ;;
|
||||
Apr) echo 4 ;;
|
||||
May) echo 5 ;;
|
||||
Jun) echo 6 ;;
|
||||
Jul) echo 7 ;;
|
||||
Aug) echo 8 ;;
|
||||
Sep) echo 9 ;;
|
||||
Oct) echo 10 ;;
|
||||
Nov) echo 11 ;;
|
||||
Dec) echo 12 ;;
|
||||
*) echo 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
# Purpose: Calculate the number of seconds between two dates
|
||||
# Arguments:
|
||||
# $1 -> Date #1
|
||||
# $2 -> Date #2
|
||||
#############################################################################
|
||||
date_diff()
|
||||
{
|
||||
if [ "${1}" != "" ] && [ "${2}" != "" ]; then
|
||||
echo $((${2} - ${1}))
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Purpose: Print a line with the expiraton interval
|
||||
# Arguments:
|
||||
# $1 -> Hostname
|
||||
# $2 -> TCP Port
|
||||
# $3 -> Status of certification (e.g., expired or valid)
|
||||
# $4 -> Date when certificate will expire
|
||||
# $5 -> Days left until the certificate will expire
|
||||
# $6 -> Issuer of the certificate
|
||||
# $7 -> Common Name
|
||||
# $8 -> Serial Number
|
||||
#####################################################################
|
||||
prints()
|
||||
{
|
||||
if [ "${NAGIOSSUMMARY}" = "TRUE" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
|
||||
MIN_DATE=$(echo "$4" | "${AWK}" '{ printf "%3s %2d %4d", $1, $2, $4 }')
|
||||
if [ "${NAGIOS}" = "TRUE" ]; then
|
||||
${PRINTF} "%-35s %-17s %-8s %-11s %s\n" "$1:$2" "$6" "$3" "$MIN_DATE" "|days=$5"
|
||||
else
|
||||
${PRINTF} "%-35s %-17s %-8s %-11s %4d\n" "$1:$2" "$6" "$3" "$MIN_DATE" "$5"
|
||||
fi
|
||||
elif [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
|
||||
${PRINTF} "%-35s %-35s %-32s %-17s\n" "$1:$2" "$7" "$8" "$6"
|
||||
|
||||
elif [ "${QUIET}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
|
||||
MIN_DATE=$(echo "$4" | "${AWK}" '{ printf "%3s %2d, %4d", $1, $2, $4 }')
|
||||
if [ "${NAGIOS}" = "TRUE" ]; then
|
||||
${PRINTF} "%-47s %-12s %-12s %s\n" "$1:$2" "$3" "$MIN_DATE" "|days=$5"
|
||||
else
|
||||
${PRINTF} "%-47s %-12s %-12s %4d\n" "$1:$2" "$3" "$MIN_DATE" "$5"
|
||||
fi
|
||||
elif [ "${QUIET}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
|
||||
${PRINTF} "%-35s %-35s %-32s\n" "$1:$2" "$7" "$8"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
####################################################
|
||||
# Purpose: Print a heading with the relevant columns
|
||||
# Arguments:
|
||||
# None
|
||||
####################################################
|
||||
print_heading()
|
||||
{
|
||||
if [ "${NOHEADER}" != "TRUE" ]; then
|
||||
if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
|
||||
${PRINTF} "\n%-35s %-17s %-8s %-11s %-4s\n" "Host" "Issuer" "Status" "Expires" "Days"
|
||||
echo "----------------------------------- ----------------- -------- ----------- ----"
|
||||
|
||||
elif [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
|
||||
${PRINTF} "\n%-35s %-35s %-32s %-17s\n" "Host" "Common Name" "Serial #" "Issuer"
|
||||
echo "----------------------------------- ----------------------------------- -------------------------------- -----------------"
|
||||
|
||||
elif [ "${QUIET}" != "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
|
||||
${PRINTF} "\n%-47s %-12s %-12s %-4s\n" "Host" "Status" "Expires" "Days"
|
||||
echo "----------------------------------------------- ------------ ------------ ----"
|
||||
|
||||
elif [ "${QUIET}" != "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
|
||||
${PRINTF} "\n%-35s %-35s %-32s\n" "Host" "Common Name" "Serial #"
|
||||
echo "----------------------------------- ----------------------------------- --------------------------------"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
####################################################
|
||||
# Purpose: Print a summary for nagios
|
||||
# Arguments:
|
||||
# None
|
||||
####################################################
|
||||
print_summary()
|
||||
{
|
||||
if [ "${NAGIOSSUMMARY}" != "TRUE" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ${SUMMARY_WILL_EXPIRE} -eq 0 ] && [ ${SUMMARY_EXPIRED} -eq 0 ]; then
|
||||
${PRINTF} "%s valid certificate(s)|days=%s\n" "${SUMMARY_VALID}" "${SUMMARY_MIN_DIFF}"
|
||||
|
||||
elif [ ${SUMMARY_EXPIRED} -ne 0 ]; then
|
||||
${PRINTF} "%s certificate(s) expired (%s:%s on %s)|days=%s\n" "${SUMMARY_EXPIRED}" "${SUMMARY_MIN_HOST}" "${SUMMARY_MIN_PORT}" "${SUMMARY_MIN_DATE}" "${SUMMARY_MIN_DIFF}"
|
||||
|
||||
elif [ ${SUMMARY_WILL_EXPIRE} -ne 0 ]; then
|
||||
${PRINTF} "%s certificate(s) will expire (%s:%s on %s)|days=%s\n" "${SUMMARY_WILL_EXPIRE}" "${SUMMARY_MIN_HOST}" "${SUMMARY_MIN_PORT}" "${SUMMARY_MIN_DATE}" "${SUMMARY_MIN_DIFF}"
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
#############################################################
|
||||
# Purpose: Set returncode to value if current value is lower
|
||||
# Arguments:
|
||||
# $1 -> New returncorde
|
||||
#############################################################
|
||||
set_returncode()
|
||||
{
|
||||
if [ "${RETCODE}" -lt "${1}" ]; then
|
||||
RETCODE="${1}"
|
||||
fi
|
||||
}
|
||||
|
||||
########################################################################
|
||||
# Purpose: Set certificate counters and informations for nagios summary
|
||||
# Arguments:
|
||||
# $1 -> Status of certificate (0: valid, 1: will expire, 2: expired)
|
||||
# $2 -> Hostname
|
||||
# $3 -> TCP Port
|
||||
# $4 -> Date when certificate will expire
|
||||
# $5 -> Days left until the certificate will expire
|
||||
########################################################################
|
||||
set_summary()
|
||||
{
|
||||
if [ "${1}" -eq 0 ]; then
|
||||
SUMMARY_VALID=$((SUMMARY_VALID+1))
|
||||
elif [ "${1}" -eq 1 ]; then
|
||||
SUMMARY_WILL_EXPIRE=$((SUMMARY_WILL_EXPIRE+1))
|
||||
else
|
||||
SUMMARY_EXPIRED=$((SUMMARY_EXPIRED+1))
|
||||
fi
|
||||
|
||||
if [ "${5}" -lt "${SUMMARY_MIN_DIFF}" ] || [ "${SUMMARY_MIN_DIFF}" -eq 0 ]; then
|
||||
SUMMARY_MIN_DATE="${4}"
|
||||
SUMMARY_MIN_DIFF="${5}"
|
||||
SUMMARY_MIN_HOST="${2}"
|
||||
SUMMARY_MIN_PORT="${3}"
|
||||
fi
|
||||
}
|
||||
|
||||
##########################################
|
||||
# Purpose: Describe how the script works
|
||||
# Arguments:
|
||||
# None
|
||||
##########################################
|
||||
usage()
|
||||
{
|
||||
echo "Usage: $0 [ -e email address ] [-E sender email address] [ -x days ] [-q] [-a] [-b] [-h] [-i] [-n] [-N] [-v]"
|
||||
echo " { [ -s common_name ] && [ -p port] } || { [ -f cert_file ] } || { [ -c cert file ] } || { [ -d cert dir ] }"
|
||||
echo ""
|
||||
echo " -a : Send a warning message through E-mail"
|
||||
echo " -b : Will not print header"
|
||||
echo " -c cert file : Print the expiration date for the PEM or PKCS12 formatted certificate in cert file"
|
||||
echo " -d cert directory : Print the expiration date for the PEM or PKCS12 formatted certificates in cert directory"
|
||||
echo " -e E-mail address : E-mail address to send expiration notices"
|
||||
echo " -E E-mail sender : E-mail address of the sender"
|
||||
echo " -f cert file : File with a list of FQDNs and ports"
|
||||
echo " -h : Print this screen"
|
||||
echo " -i : Print the issuer of the certificate"
|
||||
echo " -k password : PKCS12 file password"
|
||||
echo " -n : Run as a Nagios plugin"
|
||||
echo " -N : Run as a Nagios plugin and output one line summary (implies -n, requires -f or -d)"
|
||||
echo " -p port : Port to connect to (interactive mode)"
|
||||
echo " -q : Don't print anything on the console"
|
||||
echo " -s commmon name : Server to connect to (interactive mode)"
|
||||
echo " -S : Print validation information"
|
||||
echo " -t type : Specify the certificate type"
|
||||
echo " -V : Print version information"
|
||||
echo " -x days : Certificate expiration interval (eg. if cert_date < days)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
|
||||
##########################################################################
|
||||
# Purpose: Connect to a server ($1) and port ($2) to see if a certificate
|
||||
# has expired
|
||||
# Arguments:
|
||||
# $1 -> Server name
|
||||
# $2 -> TCP port to connect to
|
||||
##########################################################################
|
||||
check_server_status() {
|
||||
|
||||
PORT="$2"
|
||||
case "$PORT" in
|
||||
smtp|25|submission|587) TLSFLAG="-starttls smtp";;
|
||||
pop3|110) TLSFLAG="-starttls pop3";;
|
||||
imap|143) TLSFLAG="-starttls imap";;
|
||||
ftp|21) TLSFLAG="-starttls ftp";;
|
||||
xmpp|5222) TLSFLAG="-starttls xmpp";;
|
||||
xmpp-server|5269) TLSFLAG="-starttls xmpp-server";;
|
||||
irc|194) TLSFLAG="-starttls irc";;
|
||||
postgres|5432) TLSFLAG="-starttls postgres";;
|
||||
mysql|3306) TLSFLAG="-starttls mysql";;
|
||||
lmtp|24) TLSFLAG="-starttls lmtp";;
|
||||
nntp|119) TLSFLAG="-starttls nntp";;
|
||||
sieve|4190) TLSFLAG="-starttls sieve";;
|
||||
ldap|389) TLSFLAG="-starttls ldap";;
|
||||
*) TLSFLAG="";;
|
||||
esac
|
||||
|
||||
if [ "${TLSSERVERNAME}" = "FALSE" ]; then
|
||||
OPTIONS="-connect ${1}:${2} $TLSFLAG"
|
||||
else
|
||||
OPTIONS="-connect ${1}:${2} -servername ${1} $TLSFLAG"
|
||||
fi
|
||||
|
||||
echo "" | "${OPENSSL}" s_client $OPTIONS 2> "${ERROR_TMP}" 1> "${CERT_TMP}"
|
||||
|
||||
if "${GREP}" -i "Connection refused" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "Connection refused" "Unknown"
|
||||
set_returncode 3
|
||||
elif "${GREP}" -i "No route to host" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "No route to host" "Unknown"
|
||||
set_returncode 3
|
||||
elif "${GREP}" -i "gethostbyname failure" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "Cannot resolve domain" "Unknown"
|
||||
set_returncode 3
|
||||
elif "${GREP}" -i "Operation timed out" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "Operation timed out" "Unknown"
|
||||
set_returncode 3
|
||||
elif "${GREP}" -i "ssl handshake failure" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "SSL handshake failed" "Unknown"
|
||||
set_returncode 3
|
||||
elif "${GREP}" -i "connect: Connection timed out" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "Connection timed out" "Unknown"
|
||||
set_returncode 3
|
||||
elif "${GREP}" -i "Name or service not known" "${ERROR_TMP}" > /dev/null; then
|
||||
prints "${1}" "${2}" "Unable to resolve the DNS name ${1}" "Unknown"
|
||||
set_returncode 3
|
||||
else
|
||||
check_file_status "${CERT_TMP}" "${1}" "${2}"
|
||||
fi
|
||||
}
|
||||
|
||||
#####################################################
|
||||
### Check the expiration status of a certificate file
|
||||
### Accepts three parameters:
|
||||
### $1 -> certificate file to process
|
||||
### $2 -> Server name
|
||||
### $3 -> Port number of certificate
|
||||
#####################################################
|
||||
check_file_status() {
|
||||
|
||||
CERTFILE="${1}"
|
||||
HOST="${2}"
|
||||
PORT="${3}"
|
||||
|
||||
### Check to make sure the certificate file exists
|
||||
if [ ! -r "${CERTFILE}" ] || [ ! -s "${CERTFILE}" ]; then
|
||||
echo "ERROR: The file named ${CERTFILE} is unreadable or doesn't exist"
|
||||
echo "ERROR: Please check to make sure the certificate for ${HOST}:${PORT} is valid"
|
||||
set_returncode 3
|
||||
return
|
||||
fi
|
||||
|
||||
### Grab the expiration date from the X.509 certificate
|
||||
if [ "${PKCSDBPASSWD}" != "" ]; then
|
||||
# Extract the certificate from the PKCS#12 database, and
|
||||
# send the informational message to /dev/null
|
||||
"${OPENSSL}" pkcs12 -nokeys -in "${CERTFILE}" \
|
||||
-out "${CERT_TMP}" -clcerts -password pass:"${PKCSDBPASSWD}" 2> /dev/null
|
||||
|
||||
# Extract the expiration date from the certificate
|
||||
CERTDATE=$("${OPENSSL}" x509 -in "${CERT_TMP}" -enddate -noout | \
|
||||
"${SED}" 's/notAfter\=//')
|
||||
|
||||
# Extract the issuer from the certificate
|
||||
CERTISSUER=$("${OPENSSL}" x509 -in "${CERT_TMP}" -issuer -noout | \
|
||||
"${AWK}" 'BEGIN {RS=", " } $0 ~ /^O =/
|
||||
{ print substr($0,5,17)}')
|
||||
|
||||
### Grab the common name (CN) from the X.509 certificate
|
||||
COMMONNAME=$("${OPENSSL}" x509 -in "${CERT_TMP}" -subject -noout | \
|
||||
"${SED}" -e 's/.*CN = //' | \
|
||||
"${SED}" -e 's/, .*//')
|
||||
|
||||
### Grab the serial number from the X.509 certificate
|
||||
SERIAL=$("${OPENSSL}" x509 -in "${CERT_TMP}" -serial -noout | \
|
||||
"${SED}" -e 's/serial=//')
|
||||
else
|
||||
# Extract the expiration date from the ceriticate
|
||||
CERTDATE=$("${OPENSSL}" x509 -in "${CERTFILE}" -enddate -noout -inform "${CERTTYPE}" | \
|
||||
"${SED}" 's/notAfter\=//')
|
||||
|
||||
# Extract the issuer from the certificate
|
||||
CERTISSUER=$("${OPENSSL}" x509 -in "${CERTFILE}" -issuer -noout -inform "${CERTTYPE}" | \
|
||||
"${AWK}" 'BEGIN {RS=", " } $0 ~ /^O =/ { print substr($0,5,17)}')
|
||||
|
||||
### Grab the common name (CN) from the X.509 certificate
|
||||
COMMONNAME=$("${OPENSSL}" x509 -in "${CERTFILE}" -subject -noout -inform "${CERTTYPE}" | \
|
||||
"${SED}" -e 's/.*CN = //' | \
|
||||
"${SED}" -e 's/, .*//')
|
||||
|
||||
### Grab the serial number from the X.509 certificate
|
||||
SERIAL=$("${OPENSSL}" x509 -in "${CERTFILE}" -serial -noout -inform "${CERTTYPE}" | \
|
||||
"${SED}" -e 's/serial=//')
|
||||
fi
|
||||
|
||||
### Split the result into parameters, and pass the relevant pieces to date2julian
|
||||
set -- ${CERTDATE}
|
||||
MONTH=$(getmonth "${1}")
|
||||
|
||||
# Convert the date to seconds, and get the diff between NOW and the expiration date
|
||||
CERTJULIAN=$(date2julian "${MONTH#0}" "${2#0}" "${4}")
|
||||
CERTDIFF=$(date_diff "${NOWJULIAN}" "${CERTJULIAN}")
|
||||
|
||||
if [ "${CERTDIFF}" -lt 0 ]; then
|
||||
if [ "${ALARM}" = "TRUE" ]; then
|
||||
send_mail "${SENDER}" "${ADMIN}" "Certificate for ${HOST} \"(CN: ${COMMONNAME})\" has expired!" \
|
||||
"The SSL certificate for ${HOST} \"(CN: ${COMMONNAME})\" has expired!"
|
||||
fi
|
||||
|
||||
prints "${HOST}" "${PORT}" "Expired" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}"
|
||||
RETCODE_LOCAL=2
|
||||
|
||||
elif [ "${CERTDIFF}" -lt "${WARNDAYS}" ]; then
|
||||
if [ "${ALARM}" = "TRUE" ]; then
|
||||
send_mail "${SENDER}" "${ADMIN}" "Certificate for ${HOST} \"(CN: ${COMMONNAME})\" will expire in ${CERTDIFF} days or less" \
|
||||
"The SSL certificate for ${HOST} \"(CN: ${COMMONNAME})\" will expire on ${CERTDATE}"
|
||||
fi
|
||||
prints "${HOST}" "${PORT}" "Expiring" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}"
|
||||
RETCODE_LOCAL=1
|
||||
|
||||
else
|
||||
prints "${HOST}" "${PORT}" "Valid" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}"
|
||||
RETCODE_LOCAL=0
|
||||
fi
|
||||
|
||||
set_returncode "${RETCODE_LOCAL}"
|
||||
MIN_DATE=$(echo "${CERTDATE}" | "${AWK}" '{ print $1, $2, $4 }')
|
||||
set_summary "${RETCODE_LOCAL}" "${HOST}" "${PORT}" "${MIN_DATE}" "${CERTDIFF}"
|
||||
}
|
||||
|
||||
#################################
|
||||
### Start of main program
|
||||
#################################
|
||||
while getopts abc:d:e:E:f:hik:nNp:qs:St:Vx: option
|
||||
do
|
||||
case "${option}" in
|
||||
a) ALARM="TRUE";;
|
||||
b) NOHEADER="TRUE";;
|
||||
c) CERTFILE=${OPTARG};;
|
||||
d) CERTDIRECTORY=${OPTARG};;
|
||||
e) ADMIN=${OPTARG};;
|
||||
E) SENDER=${OPTARG};;
|
||||
f) SERVERFILE=$OPTARG;;
|
||||
h) usage
|
||||
exit 1;;
|
||||
i) ISSUER="TRUE";;
|
||||
k) PKCSDBPASSWD=${OPTARG};;
|
||||
n) NAGIOS="TRUE";;
|
||||
N) NAGIOS="TRUE"
|
||||
NAGIOSSUMMARY="TRUE";;
|
||||
p) PORT=$OPTARG;;
|
||||
q) QUIET="TRUE";;
|
||||
s) HOST=$OPTARG;;
|
||||
S) VALIDATION="TRUE";;
|
||||
t) CERTTYPE=$OPTARG;;
|
||||
V) echo "${PROGRAMVERSION}"
|
||||
exit 0
|
||||
;;
|
||||
x) WARNDAYS=$OPTARG;;
|
||||
\?) usage
|
||||
exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
### Check to make sure a openssl utility is available
|
||||
if [ ! -f "${OPENSSL}" ]; then
|
||||
echo "ERROR: The openssl binary does not exist in ${OPENSSL}."
|
||||
echo "FIX: Please modify the \${OPENSSL} variable in the program header."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Check to make sure a date utility is available
|
||||
if [ ! -f "${DATE}" ]; then
|
||||
echo "ERROR: The date binary does not exist in ${DATE} ."
|
||||
echo "FIX: Please modify the \${DATE} variable in the program header."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Check to make sure a grep and find utility is available
|
||||
if [ ! -f "${GREP}" ] || [ ! -f "${FIND}" ]; then
|
||||
echo "ERROR: Unable to locate the grep and find binary."
|
||||
echo "FIX: Please modify the \${GREP} and \${FIND} variables in the program header."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Check to make sure the mktemp and printf utilities are available
|
||||
if [ ! -f "${MKTEMP}" ] || [ -z "${PRINTF}" ]; then
|
||||
echo "ERROR: Unable to locate the mktemp or printf binary."
|
||||
echo "FIX: Please modify the \${MKTEMP} and \${PRINTF} variables in the program header."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Check to make sure the sed and awk binaries are available
|
||||
if [ ! -f "${SED}" ] || [ ! -f "${AWK}" ]; then
|
||||
echo "ERROR: Unable to locate the sed or awk binary."
|
||||
echo "FIX: Please modify the \${SED} and \${AWK} variables in the program header."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Check to make sure a mail client is available it automated notifications are requested
|
||||
if [ "${ALARM}" = "TRUE" ] && [ ! -f "${MAIL}" ]; then
|
||||
echo "ERROR: You enabled automated alerts, but the mail binary could not be found."
|
||||
echo "FIX: Please modify the ${MAIL} variable in the program header."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Send along the servername when TLS is used
|
||||
if ${OPENSSL} s_client -help 2>&1 | grep '-servername' > /dev/null; then
|
||||
TLSSERVERNAME="TRUE"
|
||||
else
|
||||
TLSSERVERNAME="FALSE"
|
||||
fi
|
||||
|
||||
# Place to stash temporary files
|
||||
CERT_TMP=$($MKTEMP /var/tmp/cert.XXXXXX)
|
||||
ERROR_TMP=$($MKTEMP /var/tmp/error.XXXXXX)
|
||||
|
||||
### Baseline the dates so we have something to compare to
|
||||
MONTH=$(${DATE} "+%m")
|
||||
DAY=$(${DATE} "+%d")
|
||||
YEAR=$(${DATE} "+%Y")
|
||||
NOWJULIAN=$(date2julian "${MONTH#0}" "${DAY#0}" "${YEAR}")
|
||||
|
||||
### Touch the files prior to using them
|
||||
if [ -n "${CERT_TMP}" ] && [ -n "${ERROR_TMP}" ]; then
|
||||
touch "${CERT_TMP}" "${ERROR_TMP}"
|
||||
else
|
||||
echo "ERROR: Problem creating temporary files"
|
||||
echo "FIX: Check that mktemp works on your system"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### If a HOST was passed on the cmdline, use that value
|
||||
if [ "${HOST}" != "" ]; then
|
||||
print_heading
|
||||
check_server_status "${HOST}" "${PORT:=443}"
|
||||
print_summary
|
||||
### If a file is passed to the "-f" option on the command line, check
|
||||
### each certificate or server / port combination in the file to see if
|
||||
### they are about to expire
|
||||
elif [ -f "${SERVERFILE}" ]; then
|
||||
print_heading
|
||||
|
||||
IFS=$'\n'
|
||||
for LINE in $(grep -E -v '(^#|^$)' "${SERVERFILE}")
|
||||
do
|
||||
HOST=${LINE%% *}
|
||||
PORT=${LINE##* }
|
||||
IFS=" "
|
||||
if [ "$PORT" = "FILE" ]; then
|
||||
check_file_status "${HOST}" "FILE" "${HOST}"
|
||||
else
|
||||
check_server_status "${HOST}" "${PORT}"
|
||||
fi
|
||||
done
|
||||
IFS="${OLDIFS}"
|
||||
print_summary
|
||||
### Check to see if the certificate in CERTFILE is about to expire
|
||||
elif [ "${CERTFILE}" != "" ]; then
|
||||
print_heading
|
||||
check_file_status "${CERTFILE}" "FILE" "${CERTFILE}"
|
||||
print_summary
|
||||
|
||||
### Check to see if the certificates in CERTDIRECTORY are about to expire
|
||||
elif [ "${CERTDIRECTORY}" != "" ] && ("${FIND}" -L "${CERTDIRECTORY}" -type f > /dev/null 2>&1); then
|
||||
print_heading
|
||||
for FILE in $("${FIND}" -L "${CERTDIRECTORY}" -type f); do
|
||||
check_file_status "${FILE}" "FILE" "${FILE}"
|
||||
done
|
||||
print_summary
|
||||
### There was an error, so print a detailed usage message and exit
|
||||
else
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Exit with a success indicator
|
||||
if [ "${NAGIOS}" = "TRUE" ]; then
|
||||
exit "${RETCODE}"
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
@@ -16,7 +16,7 @@ more_set_headers "X-Frame-Options: SAMEORIGIN";
|
||||
more_set_headers "X-Content-Type-Options: nosniff";
|
||||
more_set_headers "Referrer-Policy: no-referrer-when-downgrade";
|
||||
# These are dummy restrictions (meaningless headers) below.
|
||||
more_set_headers "Content-Security-Policy: img-src *";
|
||||
more_set_headers "Content-Security-Policy: img-src * data: blob:; ";
|
||||
more_set_headers "Permissions-Policy: geolocation=*";
|
||||
|
||||
# https://amalgjose.com/2020/05/15/how-to-set-the-allowed-url-length-for-a-nginx-request-error-code-414-uri-too-large/
|
||||
|
||||
@@ -23,7 +23,7 @@ server {
|
||||
|
||||
# Let's Encrypt (acme.sh) support.
|
||||
location ~ ^(/\.well-known/(?!(caldav|carddav|nodeinfo|webfinger)).*$) {
|
||||
proxy_pass http://localhost:8100;
|
||||
proxy_pass http://$PAR_ACMEHOST:$PAR_ACMEPORT;
|
||||
error_page 500 502 503 504 @proxy_error;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# This is a shell script excerpt for configuration purposes only.
|
||||
# Handle with care! Please don't put code here, only variables.
|
||||
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
|
||||
DOIT="yes" # if empty the script makes a dry run
|
||||
RETAIN_DAYS=7 # retains all files created within that many days
|
||||
RETAIN_DAYS=1 # retains all files created within that many days
|
||||
RETAIN_WEEKS=0 # retains one file per week/month,
|
||||
RETAIN_MONTHS=0 # created within that many weeks/months
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# This is a shell script excerpt for configuration purposes only.
|
||||
# Handle with care! Please don't put code here, only variables.
|
||||
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
|
||||
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
|
||||
DOIT="yes" # if empty the script makes a dry run
|
||||
# RETAIN_DAYS=7 # retains all files created within that many days
|
||||
# RETAIN_WEEKS=4 # retains one file per week/month,
|
||||
# RETAIN_MONTHS=12 # created within that many weeks/months
|
||||
|
||||
|
||||
95
tools/check_certificates
Executable file
95
tools/check_certificates
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A humble script to check the expiration of this web service certificates.
|
||||
# It uses the ssl-cert-check worker utility, which should be somewhere in path.
|
||||
# Sends an email notification when any certificate is about to expire.
|
||||
# See https://github.com/Matty9191/ssl-cert-check for details.
|
||||
#
|
||||
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# 2026-02-16 v0.1 Initial release
|
||||
|
||||
# Accepted environment variables and their defaults.
|
||||
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
|
||||
|
||||
# Other initialisations.
|
||||
ALERTEMAIL="$USER@$HOSTNAME" # Alert notification recipient
|
||||
ALERTEXPIRY=14 # Days to alert before expiry
|
||||
CERTFILES="-name *.cer -o -name *.crt -o -name *.pem"
|
||||
CERTFOLDERS="configs/acme configs/certs" # Files and folders to check
|
||||
WORKERNAME="ssl-cert-check" # The 3rd party worker script
|
||||
YMLFILE="docker-compose.yml"
|
||||
|
||||
# Messages.
|
||||
MSG_MISSINGDEP="Fatal: missing dependency"
|
||||
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
|
||||
|
||||
# Basic environment settings.
|
||||
LANG=C
|
||||
LC_ALL=C
|
||||
|
||||
# Checks the dependencies.
|
||||
TR=$(which tr 2>/dev/null)
|
||||
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
|
||||
for item in basename dirname find readlink
|
||||
do
|
||||
if [ -n "$(which $item)" ]
|
||||
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
|
||||
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
|
||||
done
|
||||
# All dependencies are available via "$THECOMMAND" (upper case) call.
|
||||
|
||||
# Where I'm?
|
||||
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do
|
||||
# resolve $SOURCE until the file is no longer a symlink
|
||||
SCRPATH="$( cd -P "$( "$DIRNAME" "$SOURCE" )" && pwd )" #"
|
||||
SOURCE="$("$READLINK" "$SOURCE")"
|
||||
# if $SOURCE was a relative symlink, we need to resolve it
|
||||
# relative to the path where the symlink file was located
|
||||
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
|
||||
done; SCRPATH="$( cd -P "$( "$DIRNAME" "$SOURCE" )" && pwd )" #"
|
||||
SCRFILE="$("$BASENAME" "$(test -L "$0" && "$READLINK" "$0" || echo "$0")")" #"
|
||||
|
||||
# Searches the base folder, containing a docker-compose.yml file.
|
||||
# Called from the base folder (./)?
|
||||
BASE_DIR="$PAR_BASEDIR"
|
||||
TEST_DIR="$SCRPATH"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# Called from ./tools/*.d?
|
||||
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
|
||||
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
|
||||
# On failure gives it up here.
|
||||
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
|
||||
echo "$MSG_MISSINGYML" >&2; exit 1
|
||||
fi
|
||||
|
||||
# Locates the worker script.
|
||||
WORKERSCRIPT="$SCRPATH/$WORKERNAME"
|
||||
[[ ! -x "$WORKERSCRIPT" ]] && WORKERSCRIPT="$(which "$WORKERNAME")"
|
||||
[[ ! -x "$WORKERSCRIPT" ]] \
|
||||
&& echo -e "$MSG_MISSINGDEP $WORKERNAME." >&2 \
|
||||
&& exit 1
|
||||
|
||||
# Collects the certificates to be check.
|
||||
certificates=""
|
||||
for folder in $CERTFOLDERS ""
|
||||
do
|
||||
if [ -n "$folder" ]; then
|
||||
certificates+="$("$FIND" "$BASE_DIR/$folder" \( $CERTFILES \) 2>/dev/null) "
|
||||
fi
|
||||
done
|
||||
|
||||
# Enumerates and checks the collected certificates.
|
||||
# Sends an email notification when expiration is approaching.
|
||||
for cert in $certificates ""
|
||||
do
|
||||
if [ -n "$cert" ]; then
|
||||
"$WORKERSCRIPT" -c "$cert" -x $ALERTEXPIRY -qae "$ALERTEMAIL"
|
||||
fi
|
||||
done
|
||||
|
||||
# That's all, Folks! :)
|
||||
@@ -10,8 +10,10 @@
|
||||
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
|
||||
# Kovács Zoltán <kovacsz@marcusconsulting.hu>
|
||||
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
# 2026-02-16 v1.1
|
||||
# new: It now also calls the SSL web certificate expiration checker utility.
|
||||
# 2023-06-18 v1.0
|
||||
# new: forked from the "Smartfront's DOCKER_skeleton" repository.
|
||||
# new: Forked from the "Smartfront's DOCKER_skeleton" repository.
|
||||
# 2021-09-14 v0.2
|
||||
# add: Checks the SSL web certificate (if any), renews it if necessary.
|
||||
# 2021-09-01 v0.1 Initial release
|
||||
@@ -24,7 +26,7 @@
|
||||
# Where I'm?
|
||||
SCRPATH="$( cd -P "$( "$(which dirname)" "$0" )" && echo "$PWD" )"
|
||||
|
||||
# Checks the SSL web certificate, renews it if necessery.
|
||||
# Checks the ACME-handled SSL web certificates, renews them if necessery.
|
||||
#
|
||||
# Uses the acme wrapper script located in the same directory.
|
||||
ACME="$SCRPATH/acme"
|
||||
@@ -32,7 +34,17 @@ ACMELOG="$($(which dirname) "$SCRPATH")/logs/web/acme.log"
|
||||
if [ -n "$ACME" -a -x "$ACME" ]; then
|
||||
"$ACME" --cron >> "$ACMELOG" 2>&1
|
||||
fi
|
||||
# Done with the certificate.
|
||||
# Done with the ACME certificates.
|
||||
|
||||
# Checks all SSL web certificates (ACME-handled or not) for expiration.
|
||||
# Checks the expiration of all certificates (including ACME-handled
|
||||
# and non-handled ones).
|
||||
#
|
||||
CHECKCERT="$SCRPATH/check_certificates"
|
||||
if [ -n "$CHECKCERT" -a -x "$CHECKCERT" ]; then
|
||||
"$CHECKCERT"
|
||||
fi
|
||||
# Done with certificates.
|
||||
|
||||
# Daily backup operations.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user