diff --git a/core/base.module.sh b/core/base.module.sh index a220aaa..b2cca24 100755 --- a/core/base.module.sh +++ b/core/base.module.sh @@ -107,6 +107,8 @@ function prepare.setCIS() { CIS[DOMAINDEFINITIONS]="${CIS[ROOT]}definitions/${CIS[DOMAIN]}/" CIS[DOMAINSTATES]="${CIS[ROOT]}states/${CIS[DOMAIN]}/" + CIS[COMPOSITIONS]="${CIS[DOMAINDEFINITIONS]:?"Missing DOMAINDEFINITIONS"}compositions/" + CIS[SET]="normal" # Sets the write protection of array 'CIS' declare -A -g -r CIS diff --git a/script/host/zfs/composition-sync/setupCompositionReceivingHost.sh b/script/host/zfs/composition-sync/setupCompositionReceivingHost.sh deleted file mode 100755 index f2d0f4d..0000000 --- a/script/host/zfs/composition-sync/setupCompositionReceivingHost.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -[ "$(id -u)" != "0" ] \ - && sudo "${0}" \ - && exit 0 - - - -_SETUP="$(readlink -f "${0}" 2> /dev/null)" - -# Folders always ends with an tailing '/' -_CIS_ROOT="${_SETUP%%/script/host/zfs/composition-sync/*}/" #Removes longest matching pattern '/script/host/zfs/composition-sync/*' from the end -_DOMAIN="$("${_CIS_ROOT:?"Missing CIS_ROOT"}core/printOwnDomain.sh")" -_DEFINITIONS="${_CIS_ROOT:?"Missing CIS_ROOT"}definitions/${_DOMAIN:?"Missing DOMAIN"}/" - - - -function checkPreconditions() { - [ -d "${_DEFINITIONS:?"Missing DEFINITIONS"}compositions" ] \ - && return 0 - - echo "No folder for your defined composition settings found: ${_DEFINITIONS:?"Missing DEFINITIONS"}compositions" - echo "Please create it and add your custom composition settings in there, following this convention:" - echo " 1.) './NAME_OF_THE_COMPOSITION/current-host' containing one line with the FQDN of the host running the composition." - echo " 2.) './NAME_OF_THE_COMPOSITION/composition-sync-hosts' containing a list of hosts receiving the composition, one host with its FQDN per line." - return 1 -} - - - -echo "Setup the host that receives the composition of others ... " \ - && checkPreconditions \ - && exit 0 - -exit 1 diff --git a/script/host/zfs/composition-sync/setupCompositionRunningHost.sh b/script/host/zfs/composition-sync/setupCompositionRunningHost.sh index 09ea551..1f17a8b 100755 --- a/script/host/zfs/composition-sync/setupCompositionRunningHost.sh +++ b/script/host/zfs/composition-sync/setupCompositionRunningHost.sh @@ -4,24 +4,16 @@ && sudo "${0}" \ && exit 0 - - -_SETUP="$(readlink -f "${0}" 2> /dev/null)" - -# Folders always ends with an tailing '/' -_CIS_ROOT="${_SETUP%%/script/host/zfs/composition-sync/*}/" #Removes longest matching pattern '/script/host/zfs/composition-sync/*' from the end -_CORE_SCRIPTS="${_CIS_ROOT:?"Missing CIS_ROOT"}core/" -_DOMAIN="$("${_CIS_ROOT:?"Missing CIS_ROOT"}core/printOwnDomain.sh")" -_DEFINITIONS="${_CIS_ROOT:?"Missing CIS_ROOT"}definitions/${_DOMAIN:?"Missing DOMAIN"}/" +source /cis/core/base.module.sh echo "Setup the user and permission to enable syncing compositions of this host ... " \ - && "${_CORE_SCRIPTS:?"Missing CORE_SCRIPTS"}addNormalUser.sh" composition-sync \ + && "${CIS[COREROOT]:?"Missing CORE_SCRIPTS"}addNormalUser.sh" composition-sync \ && echo \ - && "${_CORE_SCRIPTS:?"Missing CORE_SCRIPTS"}defineAuthorizedKeysOfUser.sh" "${_DEFINITIONS}" composition-sync \ + && "${CIS[COREROOT]:?"Missing CORE_SCRIPTS"}defineAuthorizedKeysOfUser.sh" "${CIS[DOMAINDEFINITIONS]}" composition-sync \ && echo \ - && "${_CORE_SCRIPTS:?"Missing CORE_SCRIPTS"}ensureUsageOfDefinitions.sh" "${_DEFINITIONS}" /etc/sudoers.d/allow-composition-sync-send \ + && "${CIS[COREROOT]:?"Missing CORE_SCRIPTS"}ensureUsageOfDefinitions.sh" "${CIS[DOMAINDEFINITIONS]}" /etc/sudoers.d/allow-composition-sync-send \ && exit 0 exit 1 diff --git a/script/host/zfs/composition-sync/setupSyncHost.sh b/script/host/zfs/composition-sync/setupSyncHost.sh new file mode 100755 index 0000000..1dd8745 --- /dev/null +++ b/script/host/zfs/composition-sync/setupSyncHost.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +[ "$(id -u)" != "0" ] \ + && sudo "${0}" \ + && exit 0 + +source /cis/core/base.module.sh + + + +function setup() { + local _COMPOSITION _COMPOSITIONS_FOLDER _CURRENT_HOST_FILE _SYNC_HOSTS_FILE + _COMPOSITION="${1}" + _COMPOSITIONS_FOLDER="${CIS[DOMAINDEFINITIONS]}compositions/" + _CURRENT_HOST_FILE="${_COMPOSITIONS_FOLDER}${_COMPOSITION}/current-host" + _SYNC_HOSTS_FILE="${_COMPOSITIONS_FOLDER}${_COMPOSITION}/composition-sync-hosts" + readonly _COMPOSITION _COMPOSITIONS_FOLDER + + echo "Setup the host that receives the composition of others ..." + echo + echo "No folder for your defined composition settings found: ${_COMPOSITIONS_FOLDER}" + echo "Please create it and add your custom composition settings in there, following this convention:" + echo " 1.) './NAME_OF_THE_COMPOSITION/current-host' containing one line with the FQDN of the host running the composition." + echo " 2.) './NAME_OF_THE_COMPOSITION/composition-sync-hosts' containing a list of hosts receiving the composition, one host with its FQDN per line." + echo + + [ -d "${_COMPOSITIONS_FOLDER}" ] \ + && echo "Definiton folder for compositions found: ${_COMPOSITIONS_FOLDER}" \ + && echo + + [ -n "${_COMPOSITION}" ] \ + && [ -f "${_CURRENT_HOST_FILE}" ] \ + && echo "Current defined host to run composition '${_COMPOSITION}' is:" \ + && cat "${_CURRENT_HOST_FILE}" \ + && echo + + [ -n "${_COMPOSITION}" ] \ + && [ -f "${_SYNC_HOSTS_FILE}" ] \ + && echo "Following hosts should sync the ZFS of this composition '${COMPOSITION}':" \ + && cat "${_SYNC_HOSTS_FILE}" \ + && echo + + echo "Optionally you can create following file:" + echo " - rollback (e.g.: date +%F > rollback) : allows the removal of the newest @SYNC snapshots of this day," + echo " as long as no normal snapshot is reached." + echo " - ssh-port (e.g.: echo 22 > ssh-port) : allows to use a custom port for the SSH connection." + echo ' - zfs-branch (e.g.: echo zpool1/persistent > zfs-branch) : allows to use a custom zfs prefix like: ${zfs-branch}/${composition}.' + return 0 +} + + + +base.set COMPOSITION "${1}" '^([a-zA-Z0-9][a-zA-Z0-9_-]*)?$' || exit 1 +setup "${COMPOSITION}" + + diff --git a/script/host/zfs/snapshot.sh b/script/host/zfs/snapshot.sh new file mode 100755 index 0000000..d889904 --- /dev/null +++ b/script/host/zfs/snapshot.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +source /cis/core/base.module.sh + + + +function cleanup() { + local _MIN_MIN _HOUR_MIN _DAY_MIN _MONTH_MIN + _MINUTELY_MIN=5 + _HOURLY_MIN=24 + _DAILY_MIN=7 + _MONTHLY_MIN=36 + readonly _MIN_MIN _HOUR_MIN _DAY_MIN _MONTH_MIN + + local _COMPOSITION _CURRENT_HOST _ZFS_BRANCH _ZFS + ls -d "${CIS[COMPOSITIONS]:?"Missing CIS_COMPOSITIONS"}"*/ | while read -r _COMPOSITION + do + _COMPOSITION="${_COMPOSITION%/}" #Remove tailing '/' if exists + _COMPOSITION="${_COMPOSITION##*/}" #Remove leading parts + _CURRENT_HOST="$(cat "${CIS[COMPOSITIONS]}${_COMPOSITION}/current-host" 2> /dev/null)" + _ZFS_BRANCH="$(cat "${CIS[COMPOSITIONS]}${_COMPOSITION}/zfs-branch" 2> /dev/null)" + _ZFS_BRANCH="${_ZFS_BRANCH%/}" #Remove tailing '/' if exists + + if [ "${_CURRENT_HOST}" == "${CIS[HOST]:?"Missing CIS_HOST"}" ]; then + _ZFS="$(zfs list -H -o name "${_ZFS_BRANCH:-"zpool1/persistent"}/${_COMPOSITION}" 2> /dev/null)" + else + _ZFS="$(zfs list -H -o name "${_ZFS_BRANCH:-"zpool1/persistent"}/${_COMPOSITION}-BACKUP" 2> /dev/null)" + fi + + [ -n "${_ZFS}" ] \ + && printf %b "Cleaning snapshots of: '${_ZFS}'\n" \ + && local _LIST=( $(zfs list -t snap -H -o name -S creation "${_ZFS}" | grep -F '@SNAP' ) ) + + # Nothing to do + [ ${#_LIST[@]} -eq 0 ] && continue + + _COUNT_MINUTELY=0 + _COUNT_HOURLY=0 + _COUNT_DAILY=0 + _COUNT_MONTHLY=0 + + for _SNAPSHOT in "${_LIST[@]}"; do + case "${_SNAPSHOT}" in + *"@SNAPMINUTELY_"*) + ((_COUNT_MINUTELY++)) + if [ ${_COUNT_MINUTELY} -gt ${_MINUTELY_MIN} ]; then + printf %s " - remove snapshot (${_COUNT_MINUTELY}): '@${_SNAPSHOT#*@}' ... " >&2 + zfs destroy "${_SNAPSHOT}" \ + && printf %b '(done)\n' \ + || printf %b '(FAIL)\n' + fi + ;; + *"@SNAPHOURLY_"*) + ((_COUNT_HOURLY++)) + if [ ${_COUNT_HOURLY} -gt ${_HOURLY_MIN} ]; then + printf %s " - remove snapshot (${_COUNT_HOURLY}): '@${_SNAPSHOT#*@}' ... " >&2 + zfs destroy "${_SNAPSHOT}" \ + && printf %b '(done)\n' \ + || printf %b '(FAIL)\n' + fi + ;; + *"@SNAPDAILY_"*) + ((_COUNT_DAILY++)) + if [ ${_COUNT_DAILY} -gt ${_DAILY_MIN} ]; then + printf %s " - remove snapshot (${_COUNT_DAILY}): '@${_SNAPSHOT#*@}' ... " >&2 + zfs destroy "${_SNAPSHOT}" \ + && printf %b '(done)\n' \ + || printf %b '(FAIL)\n' + fi + ;; + *"@SNAPMONTHLY_"*) + ((_COUNT_MONTHLY++)) + if [ ${_COUNT_MONTHLY} -gt ${_MONTHLY_MIN} ]; then + printf %s " - remove snapshot (${_COUNT_MONTHLY}): '@${_SNAPSHOT#*@}' ... " >&2 + zfs destroy "${_SNAPSHOT}" \ + && printf %b '(done)\n' \ + || printf %b '(FAIL)\n' + fi + ;; + esac + done + done +} + +function snapshot() { + local _MINUTE _HOUR _DAY _MONTH + _MINUTE="$(date -u "+%Y-%m-%d_%H:%M")Z" + _HOUR="${_MINUTE:0:13}Z" + _DAY="${_MINUTE:0:10}Z" + _MONTH="${_MINUTE:0:7}Z" + readonly _MINUTE _HOUR _DAY _MONTH + + [ ! -d /tmp/locks ] && mkdir /tmp/locks + + local _COMPOSITION _CURRENT_HOST _MODE _ZFS_BRANCH _ZFS + ls -d "${CIS[COMPOSITIONS]:?"Missing CIS_COMPOSITIONS"}"*/ | while read -r _COMPOSITION + do + _COMPOSITION="${_COMPOSITION%/}" #Remove tailing '/' if exists + _COMPOSITION="${_COMPOSITION##*/}" #Remove leading parts + _CURRENT_HOST="$(cat "${CIS[COMPOSITIONS]}${_COMPOSITION}/current-host" 2> /dev/null)" + _MODE="${1:-"$(cat "${CIS[COMPOSITIONS]}${_COMPOSITION}/snapshot-mode" 2> /dev/null)"}" + _MODE="${_MODE:-"HOURLY"}" + _ZFS_BRANCH="$(cat "${CIS[COMPOSITIONS]}${_COMPOSITION}/zfs-branch" 2> /dev/null)" + _ZFS_BRANCH="${_ZFS_BRANCH%/}" #Remove tailing '/' if exists + + [ "${_CURRENT_HOST}" != "${CIS[HOST]:?"Missing CIS_HOST"}" ] \ + && printf %b "ZFS will be skipped, because this host '${CIS[HOST]}' is not running the composition:\n" >&2 \ + && printf %b " - Composition : ${_COMPOSITION}\n" >&2 \ + && printf %b " - Current host: ${_CURRENT_HOST}\n" >&2 \ + && continue + + _ZFS="$(zfs list -H -o name "${_ZFS_BRANCH:-"zpool1/persistent"}/${_COMPOSITION}" 2> /dev/null)" + [ -z "${_ZFS}" ] \ + && printf %b "FAILURE - ZFS not found: ${_ZFS_BRANCH:-"zpool1/persistent"}/${_COMPOSITION}\n" >&2 \ + && continue + + ( + flock -n 9 || return 1 + + case "${_MODE:?"Missing MODE"}" in + MINUTELY) zfs snapshot "${_ZFS}@SNAPMINUTELY_${_MINUTE}" 2> /dev/null \ + && echo "Snapshot created: '${_ZFS}@SNAPMINUTELY_${_MINUTE}'" >&2 ;& # Forces execution to continue in the next block + HOURLY) zfs snapshot "${_ZFS}@SNAPHOURLY_${_HOUR}" 2> /dev/null \ + && echo "Snapshot created: '${_ZFS}@SNAPHOURLY_${_HOUR}'" >&2 ;& + DAILY) zfs snapshot "${_ZFS}@SNAPDAILY_${_DAY}" 2> /dev/null \ + && echo "Snapshot created: '${_ZFS}@SNAPDAILY_${_DAY}'" >&2 ;& + MONTHLY) zfs snapshot "${_ZFS}@SNAPMONTHLY_${_MONTH}" 2> /dev/null \ + && echo "Snapshot created: '${_ZFS}@SNAPMONTHLY_${_MONTH}'" >&2 ;; + NONE) ;; + *) echo "No valid mode to create snapshots: '${_MODE}'" >&2 ;; + esac + + ) 9>>/tmp/locks/snapshot.${_COMPOSITION}.lock + done +} + + + +# Parameter 1: Only one of these values (MINUTELY, HOURLY, DAILY, MONTHLY, NONE) are allowed, or empty. +base.set MODE "${1}" '^(MINUTELY|HOURLY|DAILY|MONTHLY|NONE)?$' || exit 1 + +snapshot "${MODE}" +cleanup + +exit 0