rsyncで世代管理(2)

【概要】
rsyncで世代バックアップを取得するスクリプトです。
シェルスクリプト(bash)で、
CRONに登録することを想定しています。
手動実行も可能です。
※スクリプトを使用する場合は、充分にテストしてからご利用ください

バックアップ元からバックアップ先にrsyncします。
バックアップ元または、バックアップ先をリモート(SSH接続)とすることもできます。(2022年4月に処理を更新しました)
※バックアップ先と、バックアップ元の両方をリモートにすることはできません。
バックアップ先で世代管理を行います。

(1)との違いは、rsync処理を前回世代との差分(--link-dest)にしている所です。
差分の無いものはハードリンクになります。ハードリンクはファイルの実体に対して複数設定可能です。すべてのハードリンクが無くなるとファイルの実体が削除となります。リンク数は「ls -l」コマンドで確認可能です。ハードリンクはinode番号が同じになります。inode番号については「ls -li」コマンドで確認可能です。

(例1)「-rw-r--r--」の右の「3」がハードリンク数
# ls -l aaa.txt
-rw-r--r-- 3 root root 4  4月 16 10:24 aaa.txt
(例2)一番左の数字がinode番号
# ls -li aaa.txt
286782 3 root root 4  4月 16 10:24 aaa.txt

処理の流れとしては下記になります。
◆3世代の例
 1回目:バックアップ先に世代1ディレクトリを作成してrsync
 2回目:バックアップ先に世代2ディレクトリを作成して、世代1とバックアップ元の差分をrsync
 3回目:バックアップ先に世代3ディレクトリを作成して、世代2とバックアップ元の差分をrsync
 4回目:世代1を新規の世代3にリネームして、前回の世代とバックアップ元の差分をrsync

【動作確認した環境】
Centos7.9

【前提条件】
・SSHを使用する場合はSSH公開鍵認証で接続先にパスワードなしで接続可能なこと
 ※(1)を参照ください
・SSHを使用する場合は接続元と接続先の両方にrsyncがインストールされていること

【rsyncgen2.sh】

#!/bin/bash

### Set rsync directory and generation #########################################
# Caution: source and destination cannot both be remote.

## rsync source directory
# / is not required at the end of the directory
# (exsample: DIR_SRC=/root/testsrcdir)
# (example: DIR_SRC_USER="testuser", if not use : DIR_SRC_USER="")
# (example: DIR_SRC_IP="192.168.1.10" , if not use : DIR_SRC_IP="")

DIR_SRC=<バックアップ元ディレクトリフルパス>
DIR_SRC_USER=""
DIR_SRC_IP=""

## rsync destination directory
# / is not required at the end of the directory
# (exsample: DIR_SRC=/root/testdstdir)
# (example: DIR_DST_USER="testuser", if not use : DIR_DST_USER="")
# (example: DIR_DST_IP="192.168.1.10" , if not use : DIR_DST_IP="")

DIR_DST=<バックアップ先ディレクトリフルパス>
DIR_DST_USER=""
DIR_DST_IP=""

## backup generation
# (exsample: BKUP_GEN_FULL=3)

BKUP_GEN=<数字>

################################################################################

# variable initialization
RC=0
DIR_DST_SSH_FLG=0
DIR_SRC_SSH=""
DIR_DST_SSH=""
YYYYMMDD_hhmmss=$(date '+%Y%m%d_%H%M%S')
DIR_DST_CNT=0
AGO_GEN=""
FULL_BKUP_FLG=0


# backup generation check
echo "${BKUP_GEN}" | grep -q "^[0-9]\+$"
RC=$?
if [ "${RC}" != "0" ] || [ ${BKUP_GEN} -le 0 ] ; then
  /usr/bin/logger "ERROR: rsync script NUM_SRC variable error: ${BKUP_GEN}"
  exit 1
fi

# rsync directory check
if [ -z ${DIR_SRC} ] || [ "${DIR_SRC}" = "/" ] ; then
  /usr/bin/logger "ERROR: rsync script DIR_SRC variable error: ${DIR_SRC}"
  exit 1
fi

if [ -n "${DIR_SRC_USER}" ] && [ -n "${DIR_SRC_IP}" ] ; then
  DIR_SRC_SSH="${DIR_SRC_USER}@${DIR_SRC_IP}:${DIR_SRC}"
else
  DIR_SRC_SSH="${DIR_SRC}"
fi

if [ -n "${DIR_DST_USER}" ] && [ -n "${DIR_DST_IP}" ] ; then
  DIR_DST_SSH="${DIR_DST_USER}@${DIR_DST_IP}:${DIR_DST}"
  DIR_DST_SSH_FLG=1
else
  DIR_DST_SSH="${DIR_DST}"
fi


# DIR_DST generation management
function dst_gen_mng () {
  # variable initialization
  NUM_DST=0
  NUM_DEL=0
  FNC_RC=0

  FNC_DIR_DST=$1
  FNC_BKUP_GEN=$2
  FNC_YYYYMMDD_hhmmss=$3

  # rsync directory check
  if [ -z ${FNC_DIR_DST} ] || [ "${FNC_DIR_DST}" = "/" ] ; then
    /usr/bin/logger "ERROR: rsync script FNC_DIR_DST variable error: ${FNC_DIR_DST}"
    exit 1
  fi

  # generation management
  ls -1 ${FNC_DIR_DST} | grep "^BKUP_GEN_" > /dev/null 2>&1
  FNC_RC=$?
  if [ "${FNC_RC}" = "0" ] ; then
    cd ${FNC_DIR_DST}
    NUM_DST=$(ls -1d ${FNC_DIR_DST}/BKUP_GEN_* | sort -r | wc -l)
    if [ ${NUM_DST} -gt 0 ] ; then
      # delete over 1 more generation
      if [ ${NUM_DST} -gt ${FNC_BKUP_GEN} ] ; then
        NUM_DEL=$(expr ${NUM_DST} - ${FNC_BKUP_GEN})
        ls -1d ${FNC_DIR_DST}/BKUP_GEN_* | sort -r | tail -n ${NUM_DEL} | xargs rm -rf
        FNC_RC=$?
        if [ "${FNC_RC}" != "0" ] ; then
          /usr/bin/logger "ERROR: rsync scriptrm command return code: ${FNC_RC}"
          exit 1
        fi
      fi
      
      # rename over oldest generation(reduce rsync diff)
      if [ ${NUM_DST} -ge ${FNC_BKUP_GEN} ] ; then
        NUM_DEL=$(expr ${NUM_DST} - ${FNC_BKUP_GEN})
        LAST_GEN=$(ls -1d ${FNC_DIR_DST}/BKUP_GEN_* | sort -r | tail -1)
        mv ${LAST_GEN} ${FNC_DIR_DST}/BKUP_GEN_${FNC_YYYYMMDD_hhmmss}
        FNC_RC=$?
        if [ "${FNC_RC}" != "0" ] ; then
          /usr/bin/logger "ERROR: rsync script mv command return code: ${FNC_RC}"
          exit 1
        fi
      else
        # mkdir backup direcotry
        if [ ! -d ${FNC_DIR_DST}/BKUP_GEN_${FNC_YYYYMMDD_hhmmss} ] ; then
          mkdir -p ${FNC_DIR_DST}/BKUP_GEN_${FNC_YYYYMMDD_hhmmss}
          FNC_RC=$?
          if [ "${FNC_RC}" != "0" ] ; then
            /usr/bin/logger "ERROR: rsync script mkdir: ${FNC_DIR_DST}"
            exit 1
          fi
        fi
      fi
    fi
    
  else
    
    # mkdir backup direcotry
    if [ ! -d ${FNC_DIR_DST}/BKUP_GEN_${FNC_YYYYMMDD_hhmmss} ] ; then
      mkdir -p ${FNC_DIR_DST}/BKUP_GEN_${FNC_YYYYMMDD_hhmmss}
      FNC_RC=$?
      if [ "${FNC_RC}" != "0" ] ; then
        /usr/bin/logger "ERROR: rsync script mkdir: ${FNC_DIR_DST}"
        exit 1
      fi
    fi
  fi
}


# run funciton dst_gen_mng
if [ "${DIR_DST_SSH_FLG}" = "1" ] ; then
  ssh ${DIR_DST_USER}@${DIR_DST_IP} "$(typeset -f dst_gen_mng); dst_gen_mng ${DIR_DST} ${BKUP_GEN} ${YYYYMMDD_hhmmss}"
  RC=$?
  if [ "${RC}" != "0" ] ; then
    /usr/bin/logger "ERROR: rsync script function dst_gen_mng: ssh run"
    exit 1
  fi
else
  dst_gen_mng ${DIR_DST} ${BKUP_GEN} ${YYYYMMDD_hhmmss}
  RC=$?
  if [ "${RC}" != "0" ] ; then
    /usr/bin/logger "ERROR: rsync script function dst_gen_mng: local run"
    exit 1
  fi
fi


# chcek need full rsync
if [ "${DIR_DST_SSH_FLG}" = "1" ] ; then
  DIR_DST_CNT=$(ssh ${DIR_DST_USER}@${DIR_DST_IP} "ls -1d ${DIR_DST}/BKUP_GEN_* | wc -l")
  if [ ${DIR_DST_CNT} -ge 2 ] ; then
    # get one time ago directory (remote)
    AGO_GEN=$(ssh ${DIR_DST_USER}@${DIR_DST_IP} "ls -1d ${DIR_DST}/BKUP_GEN_* | sort -r | head -2 | tail -1")
    FULL_BKUP_FLG=0
  else
    FULL_BKUP_FLG=1
  fi
else
  DIR_DST_CNT=$(ls -1d ${DIR_DST}/BKUP_GEN_* | wc -l)
  if [ ${DIR_DST_CNT} -ge 2 ] ; then
    # get one time ago directory (local)
    AGO_GEN=$(ls -1d ${DIR_DST}/BKUP_GEN_* | sort -r | head -2 | tail -1)
    FULL_BKUP_FLG=0
  else
    FULL_BKUP_FLG=1
  fi
fi

if [ "${FULL_BKUP_FLG}" = "0" ] ; then
  # run rsync diff from one time ago
  /usr/bin/rsync -ar --delete --link-dest=${AGO_GEN} ${DIR_SRC_SSH} ${DIR_DST_SSH}/BKUP_GEN_${YYYYMMDD_hhmmss}
  RC=$?
  # return code check and logging
  if [ "${RC}" = "0" ] ; then
    /usr/bin/logger "INFO: rsync script diff success ${DIR_SRC_SSH} ${DIR_DST_SSH}"
  else
    /usr/bin/logger "ERROR: rsync script diff ${DIR_SRC_SSH} ${DIR_DST_SSH} return code: ${RC}"
    exit 1
  fi
else
  # run rsync full (first time only)
  /usr/bin/rsync -ar --delete ${DIR_SRC_SSH} ${DIR_DST_SSH}/BKUP_GEN_${YYYYMMDD_hhmmss}
  RC=$?
  # return code check and logging
  if [ "${RC}" = "0" ] ; then
    /usr/bin/logger "INFO: rsync script success ${DIR_SRC_SSH} ${DIR_DST_SSH}"
  else
    /usr/bin/logger "ERROR: rsync script ${DIR_SRC_SSH} ${DIR_DST_SSH} return code: ${RC}"
    exit 1
  fi
fi

exit 0

【使用方法】
①viでシェルスクリプト作成
 viエディタでファイルを新規作成して、上記「rsyncgen2.sh」の内容を貼り付けます。
 $ vi rsyncgen2.sh
 下記を環境にあわせて編集し、保存します。
 SSH経由の場合は、「DIR_SRC_USER、DIR_SRC_IP」または「DIR_DST_USER、DIR_DST_IP」も編集してください。※両方ともリモートにすることはできません
 ・DIR_SRC=<バックアップ元ディレクトリ>
 ・DIR_DST=<バックアップ先ディレクトリ>
 ・BKUP_GEN=<数字>
②パーミッションを変更
 $ chmod 700 rsyncgen2.sh
③スクリプトを実行して動作確認
 詳細な動作確認をする場合
 「/usr/bin/bash -x rsyncgen.sh」で実行できます。
 スクリプト実行前に、スクリプトを動作確認用に編集すると安全です。
 rsyncはオプションが多数ありますので、必要に応じてスクリプト処理を変更ください。
 「/usr/bin/rsync -ar --delete ${SRC} ${DST}
 →「/usr/bin/rsync -arhvn --delete ${SRC} ${DST}」など。
 (-nはDRY RUNで実際には実行せず動作確認できます)
 また、負荷に応じて「nice」コマンドや「ionice」コマンドの利用もご検討ください。
 通常実行は下記です。
 $ ./rsyncgen2.sh
④CRON登録
 「crontab -l」で確認、「crontab -e」で編集します。編集時の操作方法はviのエディタと同じです。
 「<分> <時> <日> <月> <曜日> <シェルスクリプトのフルパス> > /dev/null 2>&1」
 例)
 45 22 * * * /root/backup/rsyncgen2.sh > /dev/null 2>&1

【注意点】
※rsyncは初回のみフルバックアップを使用、それ以外は差分バックアップを使用しています
※バックアップはバックアップ先ディレクトリ配下の「BKUP_GEN_YYYYMMDD_hhmmss」(YYYYMMDD_hhmmssは日時)に格納されます
※loggerコマンドで/var/log/messageにログを出力しています