snapsh/snapsh

312 lines
8.6 KiB
Bash
Executable File

#!/bin/bash
## snapsh
## Copyright (C) 2020 Jarno Rankinen
##
## 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 3 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.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
# Environment set up:
if [[ -e /etc/snapsh.conf ]]; then
. /etc/snapsh.conf
else
TOPLEVEL="/root/btrfs-toplevel"
SNAPSHOTS_LOCATION="/root/btrfs-toplevel/snapshots"
TIMESTAMP="$(date +%Y.%m.%d-%H.%M.%S)"
SUBVOLUME="root"
DESCRIPTION=""
fi
## In case of problems, define the path to the 'btrfs' executable here
BTRFS_EXECUTABLE=$(which btrfs)
help() {
printf "Usage:\n
snapsh [OPTIONS]
Options:
-h, --help Display this help message
-s SUBVOL, --snapshot SUBVOL Take a snapshot of subvolume named SUBVOL.
-d STR, --description STR Add a description for the snapshot displayed in the
snapshots listing.
-t TYPE, --type TYPE Set the type of snapshot, where TYPE=manual|auto|boot|backup
Can be used with -l, --list to filter results
-l, --list List snapshots
-r NUMBER, --remove NUMBER Remove snapshot NUMBER. See snapshot numbers with
snapsh -l
--rollback NUMBER Roll back to snapshot NUMBER. See snapshot numbers
with snapsh -l. Target subvolume is detected from
snapshot automatically.\n"
}
snapshot() {
EXIT_CODE=0
root_check
# Check that the subvolume storing snapshots exists
if [[ ! -d ${SNAPSHOTS_LOCATION} ]]; then
printf "Subvolume ${SNAPSHOTS_LOCATION} does not exist. Create it now?\n"
read -n 1 -p "y/n: "
if [[ "${REPLY}" == "y" ]]; then
${BTRFS_EXECUTABLE} subvolume create ${SNAPSHOTS_LOCATION}
unset ${REPLY}
else
EXIT_CODE=1
fi
else
# TYPE defaults to manual
if [[ -z "${SET_TYPE}" ]]; then
SET_TYPE="manual"
fi
printf "Creating snapshot of subvolume ${SUBVOLUME} as ${SUBVOLUME}_snapshot_${TIMESTAMP}\n"
# Create info file for listing snapshots
# Created first on the source subvolume, then deleted
printf "DATE=\"$(date)\"
SOURCE_SUBVOLUME=\"${SUBVOLUME}\"
DESCRIPTION=\"${DESCRIPTION}\"
TYPE=\"${SET_TYPE}\"\n" > ${TOPLEVEL}/${SUBVOLUME}/.snapsh
# Create readonly subvolume
${BTRFS_EXECUTABLE} subvolume snapshot -r ${TOPLEVEL}/${SUBVOLUME} ${SNAPSHOTS_LOCATION}/${SUBVOLUME}_snapshot_${TIMESTAMP}
# Delete info file from source
rm -f ${TOPLEVEL}/${SUBVOLUME}/.snapsh
printf "Snapshot created!
${SUBVOLUME}_snapshot_${TIMESTAMP}
DATE=$(date)
SOURCE_SUBVOLUME=${SUBVOLUME}
DESCRIPTION=${DESCRIPTION}
TYPE=\"${SET_TYPE}\"\n"
fi
exit ${EXIT_CODE}
}
list() {
root_check
NUM=1
printf "%6s %s %s %26s %s %s %6s %s %s\n" "Number" "|" "Time:" "|" "Source" "|" "Type" "|" "Description"
for snapshot in ${SNAPSHOTS_LOCATION}/*/; do
if [[ -z "${SET_TYPE}" ]]; then
. ${snapshot}/.snapsh
printf "%8s %32s %8s %8s %s\n" "${NUM} |" "${DATE} |" "${SOURCE_SUBVOLUME} |" "${TYPE} |" "${DESCRIPTION}"
elif [[ -n "${SET_TYPE}" ]]; then
. ${snapshot}/.snapsh
[[ "${SET_TYPE}" == "${TYPE}" ]] && printf "%8s %32s %8s %8s %s\n" "${NUM} |" "${DATE} |" "${SOURCE_SUBVOLUME} |" "${TYPE} |" "${DESCRIPTION}"
fi
let NUM=NUM+1
done
exit 0
}
remove() {
root_check
SNAPSHOTS=(${SNAPSHOTS_LOCATION}/*/)
if [[ "${REMOVE_TARGET}" -gt "${#SNAPSHOTS[@]}" ]]; then
printf "Snapshot number ${REMOVE_TARGET} does not exist.\n"
exit 1
elif [[ "${REMOVE_TARGET}" -lt 1 ]]; then
printf "Number must be greater than 0.\n"
exit 1
fi
let INDEX=${REMOVE_TARGET}-1
TARGET=${SNAPSHOTS[${INDEX}]}
. ${TARGET}/.snapsh
printf "Delete snapshot ${REMOVE_TARGET}: ${DATE}, source subvolume ${SOURCE_SUBVOLUME}, ${TYPE}, ${DESCRIPTION} (y/n)? "
read -n 1
if [[ "${REPLY}" == "y" ]]; then
printf "\n"
${BTRFS_EXECUTABLE} property set ${TARGET} ro false
${BTRFS_EXECUTABLE} subvolume delete ${TARGET}
exit 0
else
printf "\nAborted by user.\n"
exit 1
fi
}
rollback() {
root_check
SNAPSHOTS=(${SNAPSHOTS_LOCATION}/*/)
if [[ "${ROLLBACK_TARGET}" -gt "${#SNAPSHOTS[@]}" ]]; then
printf "Snapshot number ${ROLLBACK_TARGET} does not exist.\n"
exit 1
elif [[ "${ROLLBACK_TARGET}" -lt 1 ]]; then
printf "Number must be greater than 0.\n"
exit 1
fi
let INDEX=${ROLLBACK_TARGET}-1
TARGET=${SNAPSHOTS[${INDEX}]}
. ${TARGET}/.snapsh
printf "You are about to roll back to snapshot ${ROLLBACK_TARGET}: ${DATE}, subvolume ${SOURCE_SUBVOLUME}, type ${TYPE}, ${DESCRIPTION}.\nAre you sure (yes/no)? "
read
if [[ "${REPLY}" == "yes" ]]; then
unset ${REPLY}
printf "\nCreating a backup snapshot of ${SOURCE_SUBVOLUME}...\n"
# Create info file
printf "DATE=\"$(date)\"
SOURCE_SUBVOLUME=\"${SOURCE_SUBVOLUME}\"
DESCRIPTION=\"Rollback backup\"
TYPE=\"backup\"\n" > ${TOPLEVEL}/${SOURCE_SUBVOLUME}/.snapsh
# Create backup snapshot
${BTRFS_EXECUTABLE} subvolume snapshot -r ${TOPLEVEL}/${SUBVOLUME} ${SNAPSHOTS_LOCATION}/${SUBVOLUME}_snapshot_${TIMESTAMP}
rm -f ${TOPLEVEL}/${SOURCE_SUBVOLUME}/.snapsh
# Rename current subvolume
printf "Renaming ${SOURCE_SUBVOLUME} to ${SOURCE_SUBVOLUME}.backup...\n"
mv ${TOPLEVEL}/${SOURCE_SUBVOLUME} ${TOPLEVEL}/${SOURCE_SUBVOLUME}.backup
printf "Copying ${TARGET} to ${TOPLEVEL}/${SOURCE_SUBVOLUME}...\n"
${BTRFS_EXECUTABLE} subvolume snapshot ${TARGET} ${TOPLEVEL}/${SOURCE_SUBVOLUME}
printf "System needs to be restarted. Do you wish to do that now? (recommended!)? (y/n) "
read -n 1
if [[ "${REPLY}" == "y" ]]; then
systemctl reboot & exit 0
else
printf "\nPlease restart system as soon as possible. Any changes to subvolume ${SOURCE_SUBVOLUME} will not persist after rebooting.\n"
exit 1
fi
else
printf "\nAborted by user\n"
exit 1
fi
}
# Check for root permissions
root_check() {
if [[ "$UID" -ne 0 ]]; then
printf "This option needs root permission.\n"
exit 1
fi
}
# If no options are given, display help
if [[ "$#" -eq 0 ]]; then
help
exit 0
fi
# Options parsing:
OPTIONS=$(getopt -a -n snapsh -o hs:d:lr:t: --long help,snapshot:,description:,list,remove:,rollback:,type: -- "$@")
# Invalid options (getopt returns nonzero)
if [[ "$?" -ne 0 ]]; then
printf "Invalid options.\n"
help
exit 1
fi
eval set -- "$OPTIONS"
while true; do
case "$1" in
-h | --help)
help
shift
exit 0
;;
-d | --description)
DESCRIPTION="$2"
shift 2
;;
-s | --snapshot)
SUBVOLUME="$2"
SNAPSHOT=true
shift 2
;;
-l | --list)
LIST=true
shift
;;
-r | --remove)
REMOVE_TARGET="$2"
remove
shift 2
;;
--rollback)
ROLLBACK_TARGET="$2"
rollback
shift 2
;;
-t | --type)
case "$2" in
manual)
SET_TYPE="manual";;
auto)
SET_TYPE="auto";;
boot)
SET_TYPE="boot";;
backup)
SET_TYPE="backup";;
*)
printf "\nIncorrect type value.\n"
exit 1;;
esac
shift 2
;;
--)
shift
break
;;
esac
done
if [[ -n "${SNAPSHOT}" ]]; then
snapshot
elif [[ -n "${LIST}" ]]; then
list
fi