Separate config file, rollback cleanup systemd-unit #15
|
@ -14,6 +14,7 @@ Btrfs snapshot managing bash script
|
||||||
- Snapshots can be listed with `snapsh -l` or `snapsh --list`
|
- Snapshots can be listed with `snapsh -l` or `snapsh --list`
|
||||||
- Delete snapshots with the `-r` or `--remove` option. List snapshots first with `snapsh -l`, then delete snapshot with e.g. `snapsh -r 2`, where 2 is the number of the deletable snapshot in the `-l` listing. The list numbers always start from 1 and increment from there, so always check the number before deletion. Batch deletion might be implemented later.
|
- Delete snapshots with the `-r` or `--remove` option. List snapshots first with `snapsh -l`, then delete snapshot with e.g. `snapsh -r 2`, where 2 is the number of the deletable snapshot in the `-l` listing. The list numbers always start from 1 and increment from there, so always check the number before deletion. Batch deletion might be implemented later.
|
||||||
- Rollback to a snapshot with `snapsh --rollback NUMBER`, where NUMBER is the number listed in `snapsh -l`. The subvolume will be detected from the snapshot. E.g. `snapsh --rollback 14`. Script will ask to reboot system once done.
|
- Rollback to a snapshot with `snapsh --rollback NUMBER`, where NUMBER is the number listed in `snapsh -l`. The subvolume will be detected from the snapshot. E.g. `snapsh --rollback 14`. Script will ask to reboot system once done.
|
||||||
|
- Rollback renames your currently active subvolume to SUBVOLUME.backup, if you want these backups to be deleted automatically, copy the included `snapsh-post-rollback.service` to `/etc/systemd/system` and `systemctl enable snapsh-post-rollback.service`. It will check if `*.backup` subvolumes exist and delete them on every boot. This is recommended to be enabled, since rollbacks also take a "regular" readonly snapshot as `SUBVOLUME_backup_YYYY.MM.DD-hh:mm:ss`. The unit will be enabled automatically with the `--install` option which will be done later.
|
||||||
- Snapshot type displayed in listing can be set with the `-t, --type` option. When used in conjunction with `-l | --list`, it will filter the results based on the type. E.g. `snapsh -l -t manual` will list all the snapshots with the type `manual`. Type can be one of `manual, auto, boot` or `backup`. Note that only `manual` and `backup` are implemented properly at this stage, but you can still use the other types as well.
|
- Snapshot type displayed in listing can be set with the `-t, --type` option. When used in conjunction with `-l | --list`, it will filter the results based on the type. E.g. `snapsh -l -t manual` will list all the snapshots with the type `manual`. Type can be one of `manual, auto, boot` or `backup`. Note that only `manual` and `backup` are implemented properly at this stage, but you can still use the other types as well.
|
||||||
|
|
||||||
### Planned features:
|
### Planned features:
|
||||||
|
|
88
snapsh
88
snapsh
|
@ -16,16 +16,25 @@
|
||||||
## You should have received a copy of the GNU General Public License
|
## You should have received a copy of the GNU General Public License
|
||||||
## along with this program. If not, see <http://www.gnu.org/licenses/>.
|
## along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
printf "\n" # Print an empty line for readability
|
||||||
|
|
||||||
# Environment set up:
|
# Environment set up:
|
||||||
|
|
||||||
TOPLEVEL="/root/btrfs-toplevel"
|
# If config file exists, source it, otherwise use default values
|
||||||
SNAPSHOTS_LOCATION="/root/btrfs-toplevel/snapshots"
|
if [[ -e /etc/snapsh.conf ]]; then
|
||||||
|
. /etc/snapsh.conf
|
||||||
|
else
|
||||||
|
TOPLEVEL="/root/btrfs-toplevel" # Mountpoint of subvolid=6
|
||||||
|
SNAPSHOTS_LOCATION="/root/btrfs-toplevel/snapshots" # Mountpoint of subvolume for snapshots
|
||||||
|
|
||||||
|
TIMESTAMP="$(date +%Y.%m.%d-%H.%M.%S)" # Timestamp used in naming snapshots
|
||||||
|
fi # (Not the one used in --list)
|
||||||
|
|
||||||
|
## In case of problems, define the path to the 'btrfs' executable here
|
||||||
BTRFS_EXECUTABLE=$(which btrfs)
|
BTRFS_EXECUTABLE=$(which btrfs)
|
||||||
TIMESTAMP=$(date +%Y.%m.%d-%H:%M:%S)
|
|
||||||
SUBVOLUME="root"
|
|
||||||
DESCRIPTION=""
|
|
||||||
|
|
||||||
|
SUBVOLUME="root" # Default subvolume
|
||||||
|
DESCRIPTION="" # Description is blank unless set with the -d|--description option
|
||||||
|
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
|
@ -52,7 +61,7 @@ Options:
|
||||||
snapshot() {
|
snapshot() {
|
||||||
|
|
||||||
EXIT_CODE=0
|
EXIT_CODE=0
|
||||||
root_check
|
root_check #Check that script is run with root privileges.
|
||||||
|
|
||||||
# Check that the subvolume storing snapshots exists
|
# Check that the subvolume storing snapshots exists
|
||||||
if [[ ! -d ${SNAPSHOTS_LOCATION} ]]; then
|
if [[ ! -d ${SNAPSHOTS_LOCATION} ]]; then
|
||||||
|
@ -60,6 +69,7 @@ snapshot() {
|
||||||
read -n 1 -p "y/n: "
|
read -n 1 -p "y/n: "
|
||||||
|
|
||||||
if [[ "${REPLY}" == "y" ]]; then
|
if [[ "${REPLY}" == "y" ]]; then
|
||||||
|
# Create subvolume defined with SNAPSHOTS_LOCATION
|
||||||
${BTRFS_EXECUTABLE} subvolume create ${SNAPSHOTS_LOCATION}
|
${BTRFS_EXECUTABLE} subvolume create ${SNAPSHOTS_LOCATION}
|
||||||
unset ${REPLY}
|
unset ${REPLY}
|
||||||
else
|
else
|
||||||
|
@ -74,7 +84,7 @@ snapshot() {
|
||||||
printf "Creating snapshot of subvolume ${SUBVOLUME} as ${SUBVOLUME}_snapshot_${TIMESTAMP}\n"
|
printf "Creating snapshot of subvolume ${SUBVOLUME} as ${SUBVOLUME}_snapshot_${TIMESTAMP}\n"
|
||||||
|
|
||||||
# Create info file for listing snapshots
|
# Create info file for listing snapshots
|
||||||
# Created first on the source subvolume, then deleted
|
# Created first on the source subvolume, then deleted from the source
|
||||||
printf "DATE=\"$(date)\"
|
printf "DATE=\"$(date)\"
|
||||||
SOURCE_SUBVOLUME=\"${SUBVOLUME}\"
|
SOURCE_SUBVOLUME=\"${SUBVOLUME}\"
|
||||||
DESCRIPTION=\"${DESCRIPTION}\"
|
DESCRIPTION=\"${DESCRIPTION}\"
|
||||||
|
@ -122,8 +132,11 @@ list() {
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
root_check
|
root_check
|
||||||
|
|
||||||
|
# List snapshots in to array SNAPSHOTS
|
||||||
SNAPSHOTS=(${SNAPSHOTS_LOCATION}/*/)
|
SNAPSHOTS=(${SNAPSHOTS_LOCATION}/*/)
|
||||||
|
|
||||||
|
# Check that given NUMBER is a valid snapshot
|
||||||
if [[ "${REMOVE_TARGET}" -gt "${#SNAPSHOTS[@]}" ]]; then
|
if [[ "${REMOVE_TARGET}" -gt "${#SNAPSHOTS[@]}" ]]; then
|
||||||
printf "Snapshot number ${REMOVE_TARGET} does not exist.\n"
|
printf "Snapshot number ${REMOVE_TARGET} does not exist.\n"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -140,8 +153,8 @@ remove() {
|
||||||
read -n 1
|
read -n 1
|
||||||
if [[ "${REPLY}" == "y" ]]; then
|
if [[ "${REPLY}" == "y" ]]; then
|
||||||
printf "\n"
|
printf "\n"
|
||||||
${BTRFS_EXECUTABLE} property set ${TARGET} ro false
|
${BTRFS_EXECUTABLE} property set ${TARGET} ro false # Set snapshot as read-write first
|
||||||
${BTRFS_EXECUTABLE} subvolume delete ${TARGET}
|
${BTRFS_EXECUTABLE} subvolume delete ${TARGET} # Delete snapshot
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
printf "\nAborted by user.\n"
|
printf "\nAborted by user.\n"
|
||||||
|
@ -152,9 +165,10 @@ remove() {
|
||||||
|
|
||||||
|
|
||||||
rollback() {
|
rollback() {
|
||||||
root_check
|
root_check # Check root privileges
|
||||||
SNAPSHOTS=(${SNAPSHOTS_LOCATION}/*/)
|
SNAPSHOTS=(${SNAPSHOTS_LOCATION}/*/) # List snapshots to array
|
||||||
|
|
||||||
|
# Check that NUBER to roll back to is a valid snapshot
|
||||||
if [[ "${ROLLBACK_TARGET}" -gt "${#SNAPSHOTS[@]}" ]]; then
|
if [[ "${ROLLBACK_TARGET}" -gt "${#SNAPSHOTS[@]}" ]]; then
|
||||||
printf "Snapshot number ${ROLLBACK_TARGET} does not exist.\n"
|
printf "Snapshot number ${ROLLBACK_TARGET} does not exist.\n"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -167,12 +181,12 @@ rollback() {
|
||||||
TARGET=${SNAPSHOTS[${INDEX}]}
|
TARGET=${SNAPSHOTS[${INDEX}]}
|
||||||
. ${TARGET}/.snapsh
|
. ${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)? "
|
printf "\nYou are about to roll back to snapshot ${ROLLBACK_TARGET}: ${DATE}, subvolume ${SOURCE_SUBVOLUME}, type ${TYPE}, ${DESCRIPTION}.\n\nAre you sure (yes/no)? "
|
||||||
read
|
read
|
||||||
|
|
||||||
if [[ "${REPLY}" == "yes" ]]; then
|
if [[ "${REPLY}" == "yes" ]]; then
|
||||||
unset ${REPLY}
|
unset ${REPLY}
|
||||||
printf "\nCreating a backup snapshot of ${SOURCE_SUBVOLUME}...\n"
|
printf "\nCreating a backup snapshot of ${SOURCE_SUBVOLUME}...\n\n"
|
||||||
# Create info file
|
# Create info file
|
||||||
printf "DATE=\"$(date)\"
|
printf "DATE=\"$(date)\"
|
||||||
SOURCE_SUBVOLUME=\"${SOURCE_SUBVOLUME}\"
|
SOURCE_SUBVOLUME=\"${SOURCE_SUBVOLUME}\"
|
||||||
|
@ -180,9 +194,12 @@ rollback() {
|
||||||
TYPE=\"backup\"\n" > ${TOPLEVEL}/${SOURCE_SUBVOLUME}/.snapsh
|
TYPE=\"backup\"\n" > ${TOPLEVEL}/${SOURCE_SUBVOLUME}/.snapsh
|
||||||
|
|
||||||
# Create backup snapshot
|
# Create backup snapshot
|
||||||
${BTRFS_EXECUTABLE} subvolume snapshot -r ${TOPLEVEL}/${SUBVOLUME} ${SNAPSHOTS_LOCATION}/${SUBVOLUME}_snapshot_${TIMESTAMP}
|
printf "\nCreating snapshot of ${SUBVOLUME} as ${SNAPSHOTS_LOCATION}/${SUBVOLUME}_backup_${TIMESTAMP}...\n"
|
||||||
|
${BTRFS_EXECUTABLE} subvolume snapshot -r ${TOPLEVEL}/${SUBVOLUME} ${SNAPSHOTS_LOCATION}/${SUBVOLUME}_backup_${TIMESTAMP}
|
||||||
rm -f ${TOPLEVEL}/${SOURCE_SUBVOLUME}/.snapsh
|
rm -f ${TOPLEVEL}/${SOURCE_SUBVOLUME}/.snapsh
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
# Rename current subvolume
|
# Rename current subvolume
|
||||||
printf "Renaming ${SOURCE_SUBVOLUME} to ${SOURCE_SUBVOLUME}.backup...\n"
|
printf "Renaming ${SOURCE_SUBVOLUME} to ${SOURCE_SUBVOLUME}.backup...\n"
|
||||||
mv ${TOPLEVEL}/${SOURCE_SUBVOLUME} ${TOPLEVEL}/${SOURCE_SUBVOLUME}.backup
|
mv ${TOPLEVEL}/${SOURCE_SUBVOLUME} ${TOPLEVEL}/${SOURCE_SUBVOLUME}.backup
|
||||||
|
@ -190,13 +207,13 @@ rollback() {
|
||||||
printf "Copying ${TARGET} to ${TOPLEVEL}/${SOURCE_SUBVOLUME}...\n"
|
printf "Copying ${TARGET} to ${TOPLEVEL}/${SOURCE_SUBVOLUME}...\n"
|
||||||
${BTRFS_EXECUTABLE} subvolume snapshot ${TARGET} ${TOPLEVEL}/${SOURCE_SUBVOLUME}
|
${BTRFS_EXECUTABLE} subvolume snapshot ${TARGET} ${TOPLEVEL}/${SOURCE_SUBVOLUME}
|
||||||
|
|
||||||
printf "System needs to be restarted. Do you wish to do that now? (recommended!)? (y/n) "
|
printf "\nSystem needs to be restarted. Do you wish to do that now? (recommended!)? (y/n) "
|
||||||
read -n 1
|
read -n 1
|
||||||
|
|
||||||
if [[ "${REPLY}" == "y" ]]; then
|
if [[ "${REPLY}" == "y" ]]; then
|
||||||
systemctl reboot & exit 0
|
systemctl reboot & exit 0
|
||||||
else
|
else
|
||||||
printf "\nPlease restart system as soon as possible. Any changes to subvolume ${SOURCE_SUBVOLUME} will not persist after rebooting.\n"
|
printf "\n\nPlease restart system as soon as possible. Any changes to subvolume ${SOURCE_SUBVOLUME} will not persist after rebooting.\n"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -209,6 +226,34 @@ rollback() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
post-rollback() {
|
||||||
|
## This function is meant to be executed by the included systemd unit on every boot
|
||||||
|
## Outputs to systemd-journal, use journalctl -t snapsh to check output
|
||||||
|
|
||||||
|
EXIT_CODE=0
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
|
||||||
|
echo "Checking for leftover subvolumes..." | systemd-cat -t snapsh
|
||||||
|
|
||||||
|
BACKUPS=("${TOPLEVEL}/*.backup/")
|
||||||
|
|
||||||
|
if [[ -n "$BACKUPS" ]]; then
|
||||||
|
for backup in ${TOPLEVEL}/*.backup/; do
|
||||||
|
echo "${backup} found" | systemd-cat -t snapsh
|
||||||
|
echo "Deleting ${backup}..." | systemd-cat -t snapsh
|
||||||
|
${BTRFS_EXECUTABLE} subvolume delete ${backup} > /dev/null
|
||||||
|
let EXIT_CODE=${EXIT_CODE}+$?
|
||||||
|
done
|
||||||
|
exit ${EXIT_CODE}
|
||||||
|
else
|
||||||
|
echo "No leftovers found." | systemd-cat -t snapsh
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Check for root permissions
|
# Check for root permissions
|
||||||
root_check() {
|
root_check() {
|
||||||
if [[ "$UID" -ne 0 ]]; then
|
if [[ "$UID" -ne 0 ]]; then
|
||||||
|
@ -227,7 +272,7 @@ fi
|
||||||
|
|
||||||
|
|
||||||
# Options parsing:
|
# Options parsing:
|
||||||
OPTIONS=$(getopt -a -n snapsh -o hs:d:lr:t: --long help,snapshot:,description:,list,remove:,rollback:,type: -- "$@")
|
OPTIONS=$(getopt -a -n snapsh -o hs:d:lr:t: --long help,snapshot:,description:,list,remove:,rollback:,type:,post-rollback -- "$@")
|
||||||
|
|
||||||
# Invalid options (getopt returns nonzero)
|
# Invalid options (getopt returns nonzero)
|
||||||
if [[ "$?" -ne 0 ]]; then
|
if [[ "$?" -ne 0 ]]; then
|
||||||
|
@ -237,6 +282,8 @@ if [[ "$?" -ne 0 ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval set -- "$OPTIONS"
|
eval set -- "$OPTIONS"
|
||||||
|
|
||||||
|
# Loop through options until -- is reached
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
||||||
|
@ -274,6 +321,11 @@ while true; do
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
--post-rollback)
|
||||||
|
post-rollback
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
-t | --type)
|
-t | --type)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
manual)
|
manual)
|
||||||
|
@ -285,7 +337,7 @@ while true; do
|
||||||
backup)
|
backup)
|
||||||
SET_TYPE="backup";;
|
SET_TYPE="backup";;
|
||||||
*)
|
*)
|
||||||
printf "\nIncorrect type value.\n"
|
printf "\nIncorrect TYPE value.\n"
|
||||||
exit 1;;
|
exit 1;;
|
||||||
esac
|
esac
|
||||||
shift 2
|
shift 2
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Remove leftover *.backup subvolumes
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/sbin/snapsh --post-rollback
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
|
@ -0,0 +1,25 @@
|
||||||
|
## Snapsh requires the toplevel (subvolid=5) mounted somewhere
|
||||||
|
## You can define your mount point here.
|
||||||
|
## Default: "/root/btrfs-toplevel"
|
||||||
|
TOPLEVEL="/root/btrfs-toplevel"
|
||||||
|
|
||||||
|
## You should create a subvolume for snapshots. Snapsh will do this
|
||||||
|
## automatically, define the subvolume here. It is recommended
|
||||||
|
## to have the subvolume directly under the btrfs root (id=5)
|
||||||
|
## Use the full path
|
||||||
|
## Default: "/root/"
|
||||||
|
SNAPSHOTS_LOCATION="/root/btrfs-toplevel/snapshots"
|
||||||
|
|
||||||
|
## Timestamp format. See 'date --help' for formatting options.
|
||||||
|
## This will only be used for the name of the snapshot (=subvolume)
|
||||||
|
## created. E.g. with default setting snapshots will be created as
|
||||||
|
## "/root/btrfs-toplevel/snapshots/root_snapshot_2020_07_31-01.59.23"
|
||||||
|
## Default: $(date +%Y.%m.%d-%H.%M.%S)
|
||||||
|
TIMESTAMP="$(date +%Y.%m.%d-%H.%M.%S)"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Experimental:
|
||||||
|
## Define the list of subvolumes to be handled by snapsh here.
|
||||||
|
## Defaut: (root home)
|
||||||
|
SUBVOLUMES=(root home)
|
Loading…
Reference in New Issue