From 0bb776abe3dfc54b1f6428919520a587f4299e8c Mon Sep 17 00:00:00 2001 From: Ralf Zerres Date: Fri, 1 Jun 2018 16:28:08 +0200 Subject: [PATCH] dsnap-sync: rebase the project tree --- PKGBUILD | 18 + README.md | 12 +- TODO.md | 24 +- src/LICENSE | 339 +++++ src/Makefile | 32 + src/TODO.md | 20 + src/bin/dsnap-sync | 1285 +++++++++++++++++++ {bin => src/bin}/snap-sync.bash | 0 src/etc/snapper/config-templates/dsnap-sync | 58 + src/find_snapper_config | 15 + 10 files changed, 1789 insertions(+), 14 deletions(-) create mode 100644 PKGBUILD create mode 100644 src/LICENSE create mode 100644 src/Makefile create mode 100644 src/TODO.md create mode 100755 src/bin/dsnap-sync rename {bin => src/bin}/snap-sync.bash (100%) create mode 100644 src/etc/snapper/config-templates/dsnap-sync create mode 100755 src/find_snapper_config diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..9f8ab68 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,18 @@ +# Maintainer: Ralf Zerres +pkgname=dsnap-sync +pkgver=0.5.2 +pkgrel=1 +pkgdesc="Use snapper snapshots to backup to external drive" +arch=(any) +url="https://github.com/rzerres/dsnap-sync" +license=('GPL') +depends=(snapper dash) +source=(${url}/releases/download/$pkgver/$pkgname-$pkgver.tar.gz{,.sig}) +#validpgpkeys=('8535CEF3F3C38EE69555BF67E4B5E45AA3B8C5C3') +sha512sums=('bc7dc618874f2acc6e15f80960fa45c5703b0da709e3872febe1579d6965907074aca4704dbcc2545261392c1bff977a2b81d2a15e6850fefa3cd7c231f0290c' + 'SKIP') + +package() { + cd $pkgname + make SNAPPER_CONFIG=/etc/conf.d/snapper DESTDIR=$pkgdir install +} diff --git a/README.md b/README.md index ecccd4a..62ac074 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,21 @@ We do also support systemd.timer units. Please refer to related paragraph below. ## Requirements -`dsnap-sync`relies on external tools to achieve its goal. +beside the shell itself, `dsnap-sync`relies on external tools to achieve its goal. At run-time their availability is checked. Following tools are are used: -- snapper - awk +- btrfs +- findmnt - sed +- snapper +- tee +- wc + +optionaly tools + - notify-send +- pv ## Installation diff --git a/TODO.md b/TODO.md index 586986c..00b5907 100644 --- a/TODO.md +++ b/TODO.md @@ -1,20 +1,20 @@ -# snap-sync TODO # +# dsnap-sync TODO # ## open tasks ## -- snap-sync: parallel tasks per config -- snap-sync: introduce snapper function: important snapshots +- dsnap-sync: parallel tasks per config +- dsnap-sync: introduce snapper function: important snapshots Important snapshots have important=yes in the userdata let snapper cleanup/timeline mechanisms respect this ## finished tasks ## -- snap-sync: refine backupdir with --interactive -- snap-sync: visualize backup progress (using pv) -- snap-sync: use snapper to administer target synced snapshots -- snap-sync: introduce selectable subvolid option -- snap-sync: refine paramteter parsing -- snap-sync: refine functions structure -- snap-sync: port as posix compatible -- snap-sync: introduce selectable subvolid option -- snap-sync: use snapper to administer target synced snapshots +- dsnap-sync: refine backupdir with --interactive +- dsnap-sync: visualize backup progress (using pv) +- dsnap-sync: use snapper to administer target synced snapshots +- dsnap-sync: introduce selectable subvolid option +- dsnap-sync: refine paramteter parsing +- dsnap-sync: refine functions structure +- dsnap-sync: port as posix compatible +- dsnap-sync: introduce selectable subvolid option +- dsnap-sync: use snapper to administer target synced snapshots diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..23cb790 --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + 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. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..e46c501 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,32 @@ +# dsnap-sync +# https://github.com/rzerres/dsnap-sync +# Copyright (C) 2016, 2017 James W. Barnett +# Copyright (C) 2017, 2018 Ralf Zerres + +# 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. + +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., + +PKGNAME = dsnap-sync +PREFIX ?= /usr +SNAPPER_CONFIG ?= /etc/sysconfig/snapper +SNAPPER_TEMPLATES ?= /etc/snapper/config-templates + +BIN_DIR = $(DESTDIR)$(PREFIX)/bin +SYSTEMD_DIR = $(DESTDIR)$(PREFIX)/lib/systemd/system + +.PHONY: install + +install: + @./find_snapper_config || sed -i 's@^SNAPPER_CONFIG=.*@SNAPPER_CONFIG='$(SNAPPER_CONFIG)'@g' bin/$(PKGNAME) + @install -Dm755 bin/* -t $(BIN_DIR)/ + @install -Dm644 ./$(SNAPPER_TEMPLATES)/* -t $(DESTDIR)/$(SNAPPER_TEMPLATES)/ diff --git a/src/TODO.md b/src/TODO.md new file mode 100644 index 0000000..586986c --- /dev/null +++ b/src/TODO.md @@ -0,0 +1,20 @@ +# snap-sync TODO # + +## open tasks ## + +- snap-sync: parallel tasks per config +- snap-sync: introduce snapper function: important snapshots + Important snapshots have important=yes in the userdata + let snapper cleanup/timeline mechanisms respect this + +## finished tasks ## + +- snap-sync: refine backupdir with --interactive +- snap-sync: visualize backup progress (using pv) +- snap-sync: use snapper to administer target synced snapshots +- snap-sync: introduce selectable subvolid option +- snap-sync: refine paramteter parsing +- snap-sync: refine functions structure +- snap-sync: port as posix compatible +- snap-sync: introduce selectable subvolid option +- snap-sync: use snapper to administer target synced snapshots diff --git a/src/bin/dsnap-sync b/src/bin/dsnap-sync new file mode 100755 index 0000000..e472d08 --- /dev/null +++ b/src/bin/dsnap-sync @@ -0,0 +1,1285 @@ +#! /bin/sh + +# dsnap-sync +# https://github.com/rzerres/dsnap-sync +# Copyright (C) 2016, 2017 James W. Barnett +# Copyright (C) 2017, 2018 Ralf Zerres + +# 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. + +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., + +# ------------------------------------------------------------------------- + +# Takes snapshots of each snapper configuration. It then sends the snapshot to +# a location on an external drive. After the initial transfer, it does +# incremental snapshots on later calls. It's important not to delete the +# snapshot created on your system since that will be used to determine the +# difference for the next incremental snapshot. + +progname="${0##*/}" +version="0.5.2.2" + +# The following lines are modified by the Makefile or +# find_snapper_config script +#SNAPPER_CONFIG=/etc/conf.d/snapper +SNAPPER_CONFIG=/etc/conf.d/snapper +SNAPPER_TEMPLATE_DIR=/etc/snapper/config-templates +SNAPPER_CONFIG_DIR=/etc/snapper/configs + +# define fifo pipes +TMPDIR_PIPE=$(mktemp -d) +PIPE=$TMPDIR_PIPE/$progname.out +mkfifo $PIPE +systemd-cat --identifier="$progname" < $PIPE & + +BTRFS_PIPE=$TMPDIR_PIPE/btrfs.out +#mkfifo $BTRFS_PIPE +#systemd-cat --identifier="btrfs-pipe" < $BTRFS_PIPE & + +# redirect descriptors to given pipes +exec 3>$PIPE 4>$BTRFS_PIPE + +# global variables +donotify=false +answer=no +disk_count=-1 +disk_uuid_match_count=0 +disk_target_match_count=0 +disk_subvolid_match_count=0 +disk_uuid_match='' +selected_uuid='none' +selected_target='none' +selected_subvol='none' +snapper_snapshots=".snapshots" # hardcoded in snapper +snapper_snapsync_template="dsnap-sync" + +### +# functions +### + +check_prerequisites () { + + # requested binaries: + which awk >/dev/null 2>&1 || { printf "'awk' is not installed." && exit 1; } + which sed >/dev/null 2>&1 || { printf "'sed' is not installed." && exit 1; } + which tee >/dev/null 2>&1 || { printf "'tee' is not installed." && exit 1; } + which btrfs >/dev/null 2>&1 || { printf "'btrfs' is not installed." && exit 1; } + which findmnt >/dev/null 2>&1 || { printf "'findmnt' is not installed." && exit 1; } + which systemd-cat >/dev/null 2>&1 || { printf "'systemd-cat' is not installed." && exit 1; } + which snapper >/dev/null 2>&1 || { printf "'snapper' is not installed." && exit 1; } + which wc >/dev/null 2>&1 || { printf "'wc' is not installed." && exit 1; } + + # optional binaries: + which notify-send >/dev/null 2>&1 && { donotify=true; } + which pv >/dev/null 2>&1 && { do_pv_cmd=true; } + + if [ $(id -u) -ne 0 ] ; then printf "Script must be run as root\n" ; exit 1 ; fi + + if [ ! -r "$SNAPPER_CONFIG" ]; then + die "$SNAPPER_CONFIG does not exist." + fi +} + +check_snapper_failed_ids () { + local batch=${1:-$false} + + # active, non finished snapshot backups are marked with following string + # "$progname backup in progress" (snapper description field) + snapper_failed_ids=$(eval snapper --config $selected_config list --type single | awk '/'"$snap_description_running"'/ {cnt++} END {print cnt}') + if [ ${#snapper_failed_ids} -gt 0 ]; then + #if [ -n "$snapper_failed_ids" ]; then + if [ "$batch" ]; then + answer="yes" + else + #printf "\nNOTE: Found %s previous failed sync runs for '%s'.\n" "${snapper_failed_ids}" "$selected_config" | tee $PIPE + printf "\nNOTE: Found %s previous failed sync runs for '%s'.\n" "${snapper_failed_ids}" "$selected_config" + answer=no + get_answer_yes_no "Delete failed backup snapshots [y/N]? " "$answer" + fi + if [ "$answer" = "yes" ]; then + cmd2="snapper --config \"$selected_config\" list | awk '/'\"$snap_description_running\"'/ {print \$3}'" + cmd="snapper --config \"$selected_config\" delete " + $(eval $cmd $(eval $cmd2)) + fi + fi +} + +die () { + error "$@" + exit 1 +} + +error () { + printf "\n==> ERROR: %s\n" "$@" + notify_error 'Error' 'Check journal for more information.' +} >&2 + +get_answer_yes_no () { + local message="${1:-'Do you want to proceed [y/N]? '}" + local i="none" + + # hack: answer is a global variable, using it for preselection + while [ "$i" = "none" ]; do + read -r -p "$message" i + + case $i in + y|Y|yes|Yes) + answer="yes" + break + ;; + n|N|no|No) + answer="no" + break + ;; + *) + if [ -n "$answer" ]; then + i="$answer" + else + i="none" + printf "Select 'y' or 'n'.\n" + fi + ;; + esac + done +} + +get_config(){ + local config=${1:-"$TEMPLATE_DIR/$snapper_snapsync_template"} + local config_key=${2:-SUBVOLUME} + +# IFS="=" +# while read -r name value +# do +# if [ "$name" = "$config_key" ]; then +# value="$value" +# SUBVOLUME="$value" +# break +# fi +# done < $config + +} + +get_disk_infos () { + local disk_uuid + local disk_target + local fs_option + + # get mounted BTRFS infos + if [ "$($ssh findmnt --noheadings --nofsroot --target / --output FSTYPE)" = "btrfs" ]; then + # root filesystem is never seen as valid target location + exclude_uuid=$($ssh findmnt --noheadings --nofsroot --types btrfs --target / --output UUID) + disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $1}') + disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $2}') + fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | grep -v $exclude_uuid | awk '{print $2}') + else + disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list) + disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list) + fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}') + fi + + # we need at least one target disk + if [ ${#disk_targets} -eq 0 ]; then die "no suitable target disk found" + fi + + # Posix Shells do not support Array. Therefore using ... + # Pseudo-Arrays (assumption: equal number of members) + # Pseudo-Array: disk_uuid_$i + # Pseudo-Array: disk_target_$i + # Pseudo-Array: fs_options_$i + # Pseudo-Array: disk_selected_$y (reference to $i element) + # List: disk_uuid_match (reference to matching preselected uuids) + # List: disk_target_match (reference to matching preselected targets) + + # initialize our structures + i=0 + for disk_uuid in $disk_uuids; do + if [ "$disk_uuid" = "$uuid_cmdline" ]; then + if [ ${#disk_uuid_match} -gt 0 ]; then + disk_uuid_match="${disk_uuid_match} $i" + else + disk_uuid_match="$i" + fi + disk_uuid_match_count=$(($disk_uuid_match_count+1)) + fi + eval "disk_uuid_$i='$disk_uuid'" + disk_count=$(($disk_count+1)) + i=$((i+1)) + done + i=0 + for disk_target in $disk_targets; do + if [ "$disk_target" = "$target_cmdline" ]; then + disk_target_match="$i" + disk_target_match_count=$(($disk_target_match_count+1)) + fi + eval "disk_target_$i='$disk_target'" + i=$((i+1)) + done + i=0 + for fs_option in $fs_options; do + subvolid=$(eval echo \$fs_option | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') + if [ "$subvolid" = "$subvolid_cmdline" ]; then + disk_subvolid_match="$i" + disk_subvolid_match_count=$(($disk_subvolid_match_count+1)) + fi + eval "fs_options_$i='$fs_option'" + i=$((i+1)) + done +} + + +notify () { + # estimation: batch calls should just log + if [ "$donotify" = "true" ]; then + for u in $(users | sed 's/ /\n/' | sort -u); do + sudo -u $u DISPLAY=:0 \ + DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \ + notify-send -a $progname "$progname: $1" "$2" --icon="dialog-$3" + done + else + printf "%s %s\n" "$progname" "$2" + fi +} + +notify_info () { + notify "$1" "$2" "information" +} + +notify_error() { + notify "$1" "$2" "error" +} + +parse_params () { + # Evaluate given call parameters + while [ $# -gt 0 ]; do + key="$1" + case $key in + -h | --help | \-\? | --usage) + # Call usage() function. + usage + ;; + -b|--backupdir) + backupdir_cmdline="$2" + shift 2 + ;; + -c|--config) + if [ ${#selected_config} -gt 0 ]; then + selected_configs="${selected_configs} ${2}" + else + selected_configs="$2" + fi + shift 2 + ;; + --description-finished) + shift + snap_description_finished="${*}" + snap_description_finished="${snap_description_finished%% -*}" + params=$* + set -- $snap_description_finished + count=$# + set -- $params + shift $count + ;; + --description-running) + shift + snap_description_running=${*} + snap_description_running="${snap_description_running%% -*}" + params=$* + set -- $snap_description_running + count=$# + set -- $params + shift $count + ;; + -d|--description|--description-synced) + shift + snap_description_synced="${*}" + snap_description_synced="${snap_description_synced%% -*}" + params=$* + set -- $snap_description_synced + count=$# + set -- $params + shift $count + ;; + --dry-run) + dryrun=1 + shift 1 + ;; + -i|--interactive) + interactive=1 + donotify=true + shift + ;; + -n|--noconfirm|--batch) + batch=1 + do_pv_cmd=false + donotify=false + shift + ;; + --nonotify) + donotify=false + shift 1 + ;; + -p|--port) + port=$2 + shift 2 + ;; + --remote) + remote=$2 + shift 2 + ;; + -s|--subvolid|--SUBVOLID) + subvolid_cmdline="$2" + shift 2 + ;; + -t|--target|--TARGET) + target_cmdline="$2" + shift 2 + ;; + -u|--uuid|--UUID) + uuid_cmdline="$2" + shift 2 + ;; + -v|--verbose) + verbose=1 + shift 1 + ;; + --version) + printf "%s v%s\n" "$progname" "$version" + exit 0 + ;; + --backupdir=*) + backupdir_cmdline=${1#*=} + shift + ;; + --config=*) + if [ ${#selected_config} -gt 0 ]; then + selected_config="${selected_config} ${1#*=}" + else + selected_config="${1#*=}" + fi + shift + ;; + --description-finished=*) + snap_description_finished="${*#*=}" + snap_description_finished="${snap_description_finished%% -*}" + params_new=${*#*=} + params_new=${params_new##${snap_description_finished}} + if [ ${#params_new} -gt 0 ]; then + set -- $snap_description_finished + count=$# + set -- $params_new + fi + ;; + --description-running=*) + snap_description_running="${*#*=}" + snap_description_running="${snap_description_running%% -*}" + params_new=${*#*=} + params_new=${params_new##${snap_description_running}} + params=$# + if [ ${#params_new} -gt 0 ]; then + set -- $snap_description_running + count=$# + set -- $params_new + fi + ;; + -d=*|--description=*|--description-synced=*) + snap_description_synced="${*#*=}" + snap_description_synced="${snap_description_synced%% -*}" + params_new=${*#*=} + params_new=${params_new##${snap_description_synced}} + if [ ${#params_new} -gt 0 ]; then + set -- $snap_description_synced + count=$# + set -- $params_new + else + break + fi + ;; + --port=*) + port=${1#*=} + shift + ;; + --remote=*) + remote=${1#*=} + shift + ;; + --subvolid=*|--SUBVOLID=*) + subvolid_cmdline=${1#*=} + shift + ;; + --target=*|--TARGET=*) + target_cmdline=${1#*=} + shift + ;; + --uuid=*|--UUID=*) + uuid_cmdline=${1#*=} + shift + ;; + --) # End of all options + shift + break + ;; + -*) + printf "WARN: Unknown option (ignored): $1" >&2 + die "Unknown option" + ;; + *) + printf "Unknown option: %s\nRun '%s -h' for valid options.\n" $key $progname + die "Unknown option" + ;; + esac + done + + # Set reasonable defaults + . $SNAPPER_CONFIG + selected_configs=${selected_configs:-$SNAPPER_CONFIGS} + + snap_description_finished=${snap_description_finished:-"dsnap-sync backup"} + snap_description_running=${snap_description_running:-"dsnap-sync in progress"} + snap_description_synced=${snap_description_synced:-"dsnap-sync last incremental"} + + uuid_cmdline=${uuid_cmdline:-"none"} + target_cmdline=${target_cmdline:-"none"} + subvolid_cmdline=${subvolid_cmdline:-"none"} + backupdir_cmdline=${backupdir_cmdline:-"none"} + + if [ -z "$remote" ]; then + ssh="" + else + ssh="ssh $remote" + if [ ! -z "$port" ]; then + ssh="$ssh -p $port" + fi + fi + + if [ "$verbose" ]; then + printf "$progname (runtime arguments)\n" + printf "for backup-source:\n" + printf " selected configs: '%s'\n" "$selected_configs" + printf "for backup-target:\n" + printf " disk UUID: '%s'\n" "$uuid_cmdline" + printf " disk TARGET: '%s'\n" "$target_cmdline" + printf " disk SUBVOLID: '%s'\n" "$subvolid_cmdline" + printf " disk Backupdir: '%s'\n" "$backupdir_cmdline" + printf " remote host: '%s'\n" "$remote" + printf " ssh options: '%s'\n" "$ssh" + + printf "Snapper Descriptions\n" + printf " backup finished: '%s'\n" "$snap_description_finished" + printf " backup running: '%s'\n" "$snap_description_running" + printf " backup synced: '%s'\n" "$snap_description_synced" + + if [ "$verbose" ]; then snap_sync_options="verbose=true"; fi + if [ "$dryrun" ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi + if [ "$nonotify" ]; then snap_sync_options="${snap_sync_options} donotify=false"; fi + if [ "$batch" ]; then + snap_sync_options="${snap_sync_options} batch=true do_pv_cmd=false" + else + snap_sync_options="${snap_sync_options} do_pv_cmd=$do_pv_cmd" + fi + if [ "$interactive" ]; then snap_sync_options="${snap_sync_options} interactive=true batch=false"; fi + printf "Options: '%s'\n" "${snap_sync_options}" + fi +} + +run_config () { + + printf "\nVerify configuration...\n" + + SNAP_SYNC_EXCLUDE=no + # loop though selected snapper configurations + # Pseudo Arrays $i -> store associated elements of selected_config + i=0 + for selected_config in $selected_configs; do + # only process existing dsnap-sync configurations + if [ ! -f "/etc/snapper/configs/$selected_config" ]; then + die "Selected snapper configuration '$selected_config' does not exist." + else + . /etc/snapper/configs/$selected_config + if [ "$SUBVOLUME" = "/" ]; then + SUBVOLUME='' + fi + count=$(eval snapper --config $selected_config list --type single | \ + awk '/'"$snap_description_synced"'/' | \ + awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {cnt++} END {print cnt}') + if [ -n "$count" ] && [ "$count" -gt 1 ]; then + printf "%s entries are marked as '%s' for snapper config '%s'\n" \ + "$count" "$snap_description_synced" "$selected_config" + printf "Pointing to target with UUID '%s' and SUBVOLID '%s'. Skipping configuration '%s'.\n" \ + "$selected_uuid" "$selected_subvol" "$selected_config" + printf "Please cleanup for further processing.\n" + error "Skipping configuration $selected_config." + selected_configs=$(echo $selected_configs | sed -e "s/\($selected_config\)//") + continue + fi + fi + + # cleanup failed former runs + check_snapper_failed_ids $batch + + if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then + continue + fi + + printf "\n" + + # get latest successfully finished snapshot + # (tagged with userdata key/value pairs) + snapper_sync_id=$(eval snapper --config "$selected_config" list --type single | \ + awk '/'"$snap_description_synced"'/' | \ + awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $1}') + if [ ${#snapper_sync_id} -gt 0 ]; then + snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot + fi + + eval "snapper_sync_id_$i='$snapper_sync_id'" + eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'" + + # verify backupdir + if [ -z "$snapper_sync_id" ]; then + printf "No backups have been performed for snapper config '%s' on target disk.\n" "$selected_config" + if [ "$backupdir_cmdline" != "none" ]; then + backupdir=$backupdir_cmdline + backup_root="$selected_target/$backupdir" + else + if [ ! $batch ]; then + read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir + if [ -z "$backupdir" ]; then + backup_root="$selected_target" + else + backup_root="$selected_target/$backupdir" + fi + else + # use sane default + if [ -z "$backup_root" ]; then + backup_root="$selected_target" + fi + fi + fi + else + if [ "$verbose" ]; then + printf "Last syncronized Snapshot-ID for '%s': %s\n" "$selected_config" "$snapper_sync_id" + printf "Last syncronized Snapshot-Path for '%s': %s\n" "$selected_config" "$snapper_sync_snapshot" + fi + + backupdir=$(snapper --config "$selected_config" list --type single | awk -F "|" '/'"$snap_description_synced"'/' | awk -F "|" '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}') + if [ "$interactive" ]; then + if [ -z "$backupdir"]; then + answer=yes + get_answer_yes_no "Keep empty backupdir [Y/n]? " "$answer" + else + get_answer_yes_no "Keep backupdir '$backupdir' [Y/n]? " "$answer" + fi + if [ "$answer" = "no" ]; then + read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir + if [ -z "$backupdir" ]; then + backup_root="$selected_target" + else + backup_root="$selected_target/$backupdir" + fi + fi + fi + if [ -z "$backupdir" ]; then + backup_root="$selected_target" + else + backup_root="$selected_target/$backupdir" + fi + fi + + eval "backup_root_$i=$backup_root" + eval "backup_dir_$i=$backupdir" + + if [ "$verbose" ]; then + if [ -n "$remote" ];then + printf "Backup-Path on remote %s: %s\n" "$remote" "$backup_root" + else + printf "Backup-Path: %s\n" "$backup_root" + fi + fi + + # acting on source system + if [ ! $dryrun ]; then + #printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE + printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" + snapper_new_id=$(snapper --config "$selected_config" create --print-number -d "$snap_description_running") + snapper_new_snapshot=$SUBVOLUME/.snapshots/$snapper_new_id/snapshot + snapper_new_info=$SUBVOLUME/.snapshots/$snapper_new_id/info.xml + sync + else + #printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE + printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config" + snapper_new_id="" + fi + + # if we want to use snapper on the target to supervise the synced snapshots + # the backup_location needs to be in a subvol ".snapshots" inside $selected_config (hardcoded in snapper) + snapper_target_config="snap-$selected_config" + snapper_target_snapshot=$backup_root/$snapper_target_config/.snapshots/$snapper_new_id + if [ -z "$remote" ]; then + printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$snapper_target_snapshot/snapshot" + else + printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$remote":"$snapper_target_snapshot/snapshot" + fi + + # save in config specific infos in pseudo Arrays + eval "snapper_new_id_$i='$snapper_new_id'" + eval "snapper_new_snapshot_$i='$snapper_new_snapshot'" + eval "snapper_new_info_$i='$snapper_new_info'" + eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'" + eval "snapper_config_$i='$selected_config'" + eval "snapper_target_config_$i='$snapper_target_config'" + eval "snapper_target_snapshot_$i='$snapper_target_snapshot'" + + cont_backup="K" + eval "snapper_activate_$i=yes" + if [ $batch ]; then + cont_backup="yes" + else + answer=yes + get_answer_yes_no "Continue with backup [Y/n]? " "$answer" + if [ "$answer" = "no" ]; then + eval "snapper_activate_$i=no" + printf "Aborting backup for this configuration.\n" + snapper --config $selected_config delete $snapper_new_id + fi + fi + i=$(($i+1)) + + done +} + +run_cleanup () { + batch="1" + + # cleanup failed runs + check_snapper_failed_ids "$batch" + + # cleanup target + #$ssh btrfs subvolume delete $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_new_id/snapshot + #$ssh rm -rf $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_new_id + + # cleanup TEMPDIR + if [ -d $TMPDIR_PIPE ]; then + rm -rf $TMPDIR_PIPE || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR_PIPE" + fi +} + +run_backup () { + # Actual backing up + #printf "\nPerforming backups...\n" | tee $PIPE + printf "\nPerforming backups...\n" + + i=-1 + for selected_config in $selected_configs; do + + i=$(($i+1)) + + SNAP_SYNC_EXCLUDE=no + + if [ -f "/etc/snapper/configs/$selected_config" ]; then + . /etc/snapper/configs/$selected_config + else + die "Selected snapper configuration '$selected_config' does not exist." + fi + + cont_backup=$(eval echo \$snapper_activate_$i) + if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then + if [ "$donotify" ]; then + notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration." + fi + continue + fi + + notify_info "Backup in progress" "Backing up data for configuration '$selected_config'." + + printf "\n" + + # retrieve config specific infos from pseudo Arrays + snapper_config=$(eval echo \$snapper_config_$i) + backup_root=$(eval echo \$backup_root_$i) + backup_dir=$(eval echo \$backup_dir_$i) + snapper_sync_id=$(eval echo \$snapper_sync_id_$i) + snapper_new_id=$(eval echo \$snapper_new_id_$i) + snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i) + snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i) + snapper_new_info=$(eval echo \$snapper_new_info_$i) + snapper_target_config=$(eval echo \$snapper_target_config_$i) + snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i) + + verify_snapper_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_new_id=$snapper_new_id" "remote=$remote" + + snapper_target_snapshot_size=$(du --sum $snapper_new_snapshot 2>/dev/null | awk -F ' ' '{print $1}') + + # settings for interactive progress status + if [ "$do_pv_cmd" = "true" ]; then + pv_options="--delay-start 2 --interval 5 --timer --rate --bytes --fineta --buffer-percent --progress" + cmd_pv="pv $pv_options --size $snapper_target_snapshot_size |" + #cmd_pv="pv $pv_options --size $snapper_target_snapshot_size | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |" + else + cmd_pv='' + fi + + if [ -z "$snapper_sync_id" ]; then + # target never received any snapshot before + if [ "$verbose" ]; then + printf "Sending first snapshot for snapper config '%s' (size=%s) ...\n" "$selected_config" "$snapper_target_snapshot_size" + fi + cmd="btrfs send $verbose_flag $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE" + if [ ! "$dryrun" ]; then + # this could take a while, depending on datasize + eval $cmd 1>/dev/null + if [ "$?" -gt 0 ]; then + printf "BTRFS_PIPE: %s" "cat $BTRFS_PIPE" + die "btrfs pipe error." + fi + else + printf "dryrun: %s\n" "$cmd" + fi + else + if [ "$verbose" ]; then verbose_flag="-v"; fi + printf "Sending incremental snapshot for snapper config '%s' ...\n" "$selected_config" + # checking if parent snapshot-id (as saved on source) is also available on target + if $ssh [ -d "$backup_root/$snapper_target_config/$snapper_snapshots/$snapper_sync_id" ]; then + # Sends the difference between the new snapshot and old synced snapshot to the + # backup location. Using the -c (clone-source) flag instead of -p (parent) tells it + # that there is an identical subvolume to the synced snapshot at the receiving + # location where it can get its data. This helps speed up the transfer. + if [ ! "$dryrun" ]; then + if [ "$verbose" ]; then + printf "btrfs-send will use snapshot '%s' on target to sync metadata for %s ...\n" "$snapper_sync_snapshot" "$snapper_new_snapshot" + fi + cmd="btrfs send $verbose_flag -c $snapper_sync_snapshot $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE" + eval $cmd 1>/dev/null + if [ "$?" -gt 0 ]; then + printf "BTRFS_PIPE: %s" "cat $BTRFS_PIPE" + die "btrfs pipe error." + fi + else + printf "dryrun: btrfs send %s -c %s %s | %s btrfs receive %s %s\n" \ + "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \ + "$remote" "$verbose_flag" "$snapper_target_snapshot" + printf "dryrun: snapper --config %s delete %s\n" "$selected_config" "$snapper_sync_id" + fi + else + # need to use source snapshot to provide metadata for target + if [ ! "$dryrun" ]; then + if [ "$verbose" ]; then + printf "btrfs-send is using snapshot '%s' from source to read metadata ...\n" "$snapper_sync_snapshot" + fi + cmd="btrfs send $verbose_flag -p $snapper_sync_snapshot $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>BTRFS_PIPE" + #$(eval $cmd) + eval $cmd 1>/dev/null + if [ "$?" -gt 0 ]; then + printf "BTRFS_PIPE: %s" "cat $BTRFS_PIPE" + #die "btrfs pipe error." + fi + #printf "btrfs returns: '%i'\n" "$ret" + else + printf "dryrun: btrfs send %s -c %s %s | %s btrfs receive %s %s\n" \ + "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \ + "$remote" "$verbose_flag" "$snapper_target_snapshot" + printf "dryrun: snapper --config %s delete %s\n" "$selected_config" "$snapper_sync_id" + fi + fi + fi + + # finally: send the snapper info metadata + if [ -z "$remote" ]; then + if [ ! "$dryrun" ]; then + cp "$snapper_new_info" "$snapper_target_snapshot" + else + cmd="cp $snapper_new_info $snapper_target_snapshot" + printf "dryrun: %s\n" "$cmd" + fi + else + if [ ! "$dryrun" ]; then + if [ -n "$port" ]; then + rsync -avzq -e "ssh -p $port" "$snapper_new_info" "$remote:$snapper_target_snapshot" + else + rsync -avzq "$snapper_new_info" "$remote":"$snapper_target_snapshot" + fi + else + if [ -n "$port" ]; then + cmd="rsync -avzq -e \"ssh -p $port\" $snapper_new_info $remote:$snapper_target_snapshot" + else + cmd="rsync -avzq $snapper_new_info $remote:$snapper_target_snapshot" + fi + printf "dryrun: %s\n" "$cmd" + fi + fi + done +} + +run_finalize () { + # Actual backing up + printf "\nFinalize backups...\n" + + i=-1 + for selected_config in $selected_configs; do + + i=$(($i+1)) + + SNAP_SYNC_EXCLUDE=no + + if [ -f "/etc/snapper/configs/$selected_config" ]; then + . /etc/snapper/configs/$selected_config + else + die "Selected snapper configuration '$selected_config' does not exist." + fi + + cont_backup=$(eval echo \$snapper_activate_$i) + if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then + if [ "$donotify" ]; then + notify_info "Finalize backup" "NOTE: Skipping '$selected_config' configuration." + fi + continue + fi + + if [ "$donotify" ]; then + notify_info "Finalize backup" "Cleanup tasks for configuration '$selected_config'." + fi + printf "\n" + + # retrieve config specific infos from pseudo Arrays + snapper_config=$(eval echo \$snapper_config_$i) + backup_root=$(eval echo \$backup_root_$i) + backup_dir=$(eval echo \$backup_dir_$i) + snapper_sync_id=$(eval echo \$snapper_sync_id_$i) + snapper_new_id=$(eval echo \$snapper_new_id_$i) + snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i) + snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i) + snapper_new_info=$(eval echo \$snapper_new_info_$i) + snapper_target_config=$(eval echo \$snapper_target_config_$i) + snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i) + + # It's important not to change the values of the key/value pairs ($userdata) + # which is stored in snappers info.xml file of the source snapshot. + # This is how we find the parent. + + src_host=$(eval cat /etc/hostname) + src_uuid=$(eval findmnt --noheadings --output UUID $SUBVOLUME) + src_subvolid=$(eval findmnt --noheadings --output OPTIONS $SUBVOLUME | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') + userdata="backupdir=$backup_dir, subvolid=$selected_subvol, uuid=$selected_uuid, host=$remote" + target_userdata="subvolid=$src_subvolid, uuid=$src_uuid, host=$src_host" + + # Tag new snapshots key/value parameter + if [ ! "$dryrun" ]; then + # source snapshot + if [ "$verbose" ]; then + printf "Tagging snapper metadata on source for configuration '%s' ...\n" "$selected_config" + fi + cmd="snapper --verbose --config \"$selected_config\" modify --description \"$snap_description_synced\" --userdata \"$userdata\" \"$snapper_new_id\"" + ret=$(eval $cmd) + #printf "return: '%s'\n" "$ret" + sync + + # target snapshot + if [ "$verbose" ]; then + printf "Tagging snapper metadata on target for configuration '%s' ...\n" "$selected_config" + fi + ii=1 + ii_max=20 + cmd="snapper --verbose --config \"$snapper_target_config\" list --type single | awk '/'\"$snap_description_running\"'/' | awk -F '|' '\$1 == "$snapper_new_id" {print \$1}'" + # !!! ugly hack !!!: wait for snapper to list target snapshot in database. how to trigger database resync? + # it is not deterministic, when the entry in the listing will show up .... for now, wait max 10 min ... + while [ "$ii" -le "$ii_max" ]; do + if [ -n "$remote" ]; then + ret=$($ssh $cmd) + else + ret=$(eval $cmd) + fi + #printf "return: '%s'\n" "$ret" + if [ -n "$ret" ]; then + if [ "$ret" -eq "$snapper_new_id" ]; then + cmd="snapper --verbose --config \"$snapper_target_config\" modify --description \"$snap_description_finished\" --userdata \"$target_userdata\" \"$snapper_new_id\"" + if [ -n "$remote" ]; then + ret=$($ssh "$cmd") + else + ret=$(eval $cmd) + fi + #printf "return: '%s'\n" "$ret" + break + fi + fi + if [ "$verbose" ]; then + printf "Waiting for snappers database update on target %s/%s ...\n" "$ii" "$ii_max" + fi + sleep 30 + ii=$(($ii + 1)) + done + else + cmd="snapper --verbose --config $selected_config modify --description $snap_description_synced --userdata $userdata $snapper_new_id" + printf "dryrun: %s\n" "$cmd" + cmd="$ssh snapper --verbose --config $snapper_target_config modify -description $snap_description_finished --userdata $target_userdata $snapper_new_id" + printf "dryrun: %s\n" "$cmd" + fi + + # Cleanup synced source snapshots + if [ -n "$snapper_sync_id" ]; then + cmd="snapper --verbose --config \"$snapper_config\" modify --description \"$snap_description_finished\" --cleanup timeline \"$snapper_sync_id\"" + ret=$(eval $cmd) + #printf "return: '%s'\n" "$ret" + sync + fi + + printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE + + done +} + +select_target_disk () { + local i=0 + local disk_id=0 + + #local disk_selected_ids='' + local disk_selected_count=0 + local subvolid='' + local subvol='' + + # print selection table + if [ -z "$remote" ]; then + printf "Selecting a mounted BTRFS device for backups on your local machine.\n" + else + printf "Selecting a mounted BTRFS device for backups on %s.\n" "$remote" + fi + while [ "$disk_id" -eq -1 ] || [ "$disk_id" -le $disk_count ]; do + if [ "$disk_subvolid_match_count" -eq 1 ]; then + # matching SUBVOLID selection from commandline + # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) + eval "disk_selected_$i='$disk_subvolid_match'" + disk=$(eval echo \$disk_uuid_$disk_subvolid_match) + subvolid=$(eval echo \$disk_subvolid_$disk_subvolid_match) + fs_options=$(eval echo \$fs_options_$disk_subvolid_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') + disk_selected=$disk_subvolid_match + break + fi + if [ "$disk_target_match_count" -eq 1 ]; then + # matching TARGET selection from commandline + # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) + eval "disk_selected_$i='$disk_target_match'" + disk=$(eval echo \$disk_uuid_$disk_target_match) + target=$(eval echo \$disk_target_$disk_target_match) + fs_options=$(eval echo \$fs_options_$disk_target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') + disk_selected=$disk_target_match + break + fi + if [ "$disk_uuid_match_count" -gt 1 ]; then + # got UUID selection from commandline + disk_count=$disk_uuid_match_count + if [ "$verbose" ]; then + printf "%s mount points were found with UUID '%s'.\n" "$disk_uuid_match_count" "$uuid_cmdline" + fi + for disk_uuid in $disk_uuid_match; do + # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) + eval "disk_selected_$i='$disk_uuid'" + disk=$(eval echo \$disk_uuid_$disk_uuid) + fs_options=$(eval echo \$fs_options_$disk_uuid | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') + printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options" + i=$((i+1)) + done + else + while [ "$disk_id" -le $disk_count ]; do + # present all mounted BTRFS filesystems + # Pseudo-Array: disk_selected_$i (reference to $disk_id element) + eval disk_selected_$i="$disk_id" + disk=$(eval echo \$disk_uuid_$disk_id) + target=$(eval echo \$disk_target_$disk_id) + fs_options=$(eval echo \$fs_options_$disk_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') + printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options" + i=$((i+1)) + disk_id=$(($disk_id+1)) + done + fi + printf "%4s) Exit\n" "x" + read -r -p "Enter a number: " disk_selected + case $disk_selected in + x) + break + ;; + [0-9][0-9]|[0-9]) + if [ "$disk_selected" -gt "$disk_count" ]; then + disk_id=0 + i=0 + else + break + fi + ;; + *) + printf "\nNo disk selected. Select a disk to continue.\n" + disk_id=0 + i=0 + ;; + esac + done + if [ "$disk_selected" = x ]; then + exit 0 + fi + + selected_uuid=$(eval echo \$disk_uuid_$disk_selected) + selected_target=$(eval echo \$disk_target_$disk_selected) + selected_subvol=$(eval echo \$fs_options_$disk_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') + if [ "$dryrun" ]; then + printf "Selected Subvol-ID=%s: %s on %s\n" "$selected_subvol" "$selected_target" "$selected_uuid" + fi +} + +set_config(){ + local config=${1:-/etc/snapper/config-templates/"$snapper_snapsync_template"} + local config_key=${2:-SUBVOLUME} + local config_value=${3:-/var/lib/dsnap-sync} + + if [ -n "$remote" ]; then + $ssh sed -i \'"s#^\($config_key\s*=\s*\).*\$#\1\"$config_value\"#"\' $config + else + sed -i "s#^\($config_key\s*=\s*\).*\$#\1\"$config_value\"#" $config + fi +} + +traperror () { + printf "Exited due to error on line %s.\n" $1 + printf "exit status: %s\n" "$2" + #printf "command: %s\n" "$3" + #printf "bash line: %s\n" "$4" + #printf "function name: %s\n" "$5" + exit 1 +} + +trapkill () { + printf "Exited due to user intervention.\n" + run_cleanup 1 + exit 0 +} + +usage () { + cat < backupdir is a relative path that will be appended to target backup-root + -d, --description Change the snapper description. Default: "latest incremental backup" + --label-finished snapper description tagging successful jobs. Default: "dsnap-sync backup" + --label-running snapper description tagging active jobs. Default: "dsnap-sync in progress" + --label-synced snapper description tagging last synced jobs. + Default: "dsnap-sync last incremental" + -c, --config Specify the snapper configuration to use. Otherwise will perform for each snapper + configuration. Can list multiple configurations within quotes, space-separated + (e.g. -c "root home"). + -n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup + --batch directory name on first backup" + --nonotify Disable graphical notification (via dbus) + -r, --remote
Send the snapshot backup to a remote machine. The snapshot will be sent via ssh. + You should specify the remote machine's hostname or ip address. The 'root' user + must be permitted to login on the remote machine. + -p, --port The remote port. + -s, --subvolid Specify the subvolume id of the mounted BTRFS subvolume to back up to. Defaults to 5. + -u, --uuid Specify the UUID of the mounted BTRFS subvolume to back up to. Otherwise will prompt." + If multiple mount points are found with the same UUID, will prompt user." + -t, --target Specify the mountpoint of the BTRFS subvolume to back up to. + --remote
Send the snapshot backup to a remote machine. The snapshot will be sent via ssh. You + should specify the remote machine's hostname or ip address. The 'root' user must be + permitted to login on the remote machine. + --dry-run perform a trial run where no changes are made. + -v, --verbose Be more verbose on what's going on. + --version show program version +EOF + + exit 1 +} + +verify_snapper_structure () { + local backup_root=${1##backup_root=} + local snapper_config=${2##snapper_target_config=} + local snapper_id=${3##snapper_new_id=} + local remote_host=${4##remote=} + + if [ "$verbose" ]; then + printf "Verify snapper filesystem structure on target %s...\n" "$remote" + fi + + # if not accessible, create backup-path + if $ssh [ ! -d $backup_root ]; then + if [ ! "$dryrun" ]; then + if [ "$verbose" ]; then + printf "Create backup-path %s:%s ...\n" "$remote_host" "$backup_root" + fi + $ssh mkdir --mode=0700 --parents $backup_root + else + printf "dryrun: Would create backup-path %s:%s ...\n" "$remote_host" "$backup_root" + fi + fi + + # verify that we have a snapper compatible structure for selected config on target + if $ssh [ ! -d $backup_root/$snapper_config ]; then + if [ "$verbose" ]; then + printf "Create new snapper capable BTRFS subvolume '%s:%s' ...\n" "$remote_host" "$backup_root/$snapper_config" + fi + if [ ! "$dryrun" ]; then + # verify that we can use a dsnap-sync aware template + if $ssh [ ! -f $SNAPPER_TEMPLATE_DIR/$snapper_snapsync_template ]; then + printf "A snapper template %s to configure the snapper subvolume %s is missing in %s on %s.\n" "$snapper_snapsync_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host" + printf "Did you miss to install the dsnap-sync's default snapper template on %s?\n" "$remote" + die "snapper template %s to configure the snapper subvolume %s is missing in %s on %s.\n" "$snapper_snapsync_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host" + fi + # create the non existing remote BTRFS subvolume + cmd="btrfs subvolume create $backup_root/$snapper_config" + $ssh $cmd || die "Creation of BTRFS subvolume %s:%s failed.\n" "$remote_host" "$backup_root/$snapper_config" + cmd="chmod 0700 $backup_root/$snapper_config" + $ssh $cmd || die "Changing the directory mode for %s on %s failed.\n" "$backup_root/$snapper_config" "$remote_host" + else + printf "dryrun: Would create new snapper structure in '%s:%s' ...\n" "$backup_root/$snapper_config" + printf "dryrun: Would create new snapper configuration from template %s ...\n" "$snapper_snapsync_template" + fi + else + cmd="$ssh stat --format=%i $backup_root/$snapper_config" + if [ $(eval $cmd) -ne 256 ]; then + die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" "$snapper_config" "$backup_root/$snapper_config" + fi + # if changed, adapt SUBVOLUME in given config + #$ssh $(. /etc/snapper/configs/$snapper_config) + #get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" + #if $ssh [ "$SUBVOLUME" != \"$backup_root/$snapper_config\" ]; then + # SUBVOLUME="$backup_root/$snapper_config" + # set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME" + #fi + fi + + # verify that we have a valid snapper config + if [ ! "$dryrun" ]; then + if $ssh [ ! -f $SNAPPER_CONFIG_DIR/$snapper_config ]; then + # snapper-logic will create $backup_root/$snapper_config/.snapshots + cmd="snapper --config $snapper_config create-config --template $snapper_snapsync_template --fstype btrfs $backup_root/$snapper_config" + $ssh $cmd || die "Creation of snapper capable config %s on %s failed.\n" "$backup_root/$snapper_config" "$remote_host" + else + # verify if SUBVOLUME needs to be updated for given snapper config + cmd="snapper list-configs | grep $snapper_config 1>/dev/null" + if $ssh [ ! $(eval $cmd) ]; then + # if changed, adapt targets SUBVOLUME path + if [ $verbose ]; then + printf "TODO: verify for SUBVOLUME update in %s\n" "$snapper_config" + fi + #get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" + #if $ssh [ "$SUBVOLUME" != \"$backup_root/$snapper_config\" ]; then + # SUBVOLUME="$backup_root/$snapper_config" + # set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME" + # cmd="btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshots" + # $ssh $cmd || die "Can't create subvolume %s in %s to hold target snapshots.\n" "$snapper_snapshots" "$backup_root/$snapper_config" + #fi + fi + if $ssh [ ! -d $backup_root/$snapper_config/$snapper_snapshots ]; then + cmd="btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshots" + $ssh $cmd || die "Can't create subvolume %s in %s to hold target snapshots.\n" "$snapper_snapshots" "$backup_root/$snapper_config" + fi + fi + else + printf "dryrun: Would check/create for valid snapper config %s ...\n" \ + "$snapper_config" + fi + + # verify that target snapshot id can take the new snapshot data + if [ ! "$dryrun" ]; then + if $ssh [ ! -d $backup_root/$snapper_config/$snapper_snapshots/$snapper_id ]; then + if [ "$verbose" ]; then + printf "Create path %s to store target snapshot.\n" "$backup_root/$snapper_config/$snapper_snapshots/$snapper_id" + fi + $ssh mkdir --mode=0700 $backup_root/$snapper_config/$snapper_snapshots/$snapper_id + else + if [ -z "$remote" ]; then + printf "Cancel Snapshot creation: Former snapshot with id '%s' already exist in '%s'\n" "$snapper_id" "$backup_root/$snapper_config/$snapper_snapshots" + else + printf "Cancel Snapshot creation: Former snapshot with id '%s' already exist on %s in '%s'\n" "$snapper_id" "$remote" "$backup_root/$snapper_config/$snapper_snapshots" + fi + + # cleanup generated snapper entry + check_snapper_failed_ids $batch + die "Can't create new snapshot with given snapshot-id!" + return=1 + fi + else + printf "dryrun: Would check/create path %s to store target snapshot ...\n" \ + "$backup_root/$snapper_config/$snapper_snapshots/$snapper_id" + fi +} + +### +# Main +### + +cwd=`pwd` +ssh="" + +# can't be ported to dash (ERR is not supported) +#trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR +trap trapkill TERM INT + +check_prerequisites + +# validate commandline options, set resonable defaults +parse_params $@ + +# read mounted BTRFS structures +get_disk_infos + +if [ "$donotify" ]; then + if [ "$target_cmdline" != "none" ]; then + if [ -z "$remote" ]; then + notify_info "Backup started" "Starting backups to '$target_cmdline' ..." + else + notify_info "Backup started" "Starting backups to '$target_cmdline' at $remote ..." + fi + elif [ "$uuid_cmdline" != "none" ]; then + if [ -z "$remote" ]; then + notify_info "Backup started" "Starting backups to $uuid_cmdline..." + else + notify_info "Backup started" "Starting backups to $uuid_cmdline at $remote..." + fi + else + if [ -z "$remote" ]; then + notify_info "Backup started" "Starting backups. Use command line menu to select disk." + else + notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote." + fi + fi +fi + +# select the target BTRFS subvol +select_target_disk + +printf "\nYou selected the disk with UUID %s (subvolid=%s).\n" "$selected_uuid" "$selected_subvol" +if [ -z "$remote" ]; then + printf "The disk is mounted at %s.\n" "$selected_target" +else + printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_target" +fi + +# create and initialize structures for snapper configs +run_config + +# run backups using btrfs-send -> btrfs-receive +run_backup + +# finalize backup tasks +run_finalize + +printf "\nDone!\n" +exec 3>&- + +# cleanup +run_cleanup + +if [ "$donotify" ]; then + if [ "$uuid_cmdline" != "none" ]; then + notify_info "Finished" "Backups to $uuid_cmdline complete!" + else + notify_info "Finished" "Backups complete!" + fi +fi diff --git a/bin/snap-sync.bash b/src/bin/snap-sync.bash similarity index 100% rename from bin/snap-sync.bash rename to src/bin/snap-sync.bash diff --git a/src/etc/snapper/config-templates/dsnap-sync b/src/etc/snapper/config-templates/dsnap-sync new file mode 100644 index 0000000..a82150a --- /dev/null +++ b/src/etc/snapper/config-templates/dsnap-sync @@ -0,0 +1,58 @@ +### +# snapper template for dsnap-sync handling +### + +# subvolume to snapshot +SUBVOLUME="/var/lib/dsnap-sync" + +# filesystem type +FSTYPE="btrfs" + + +# users and groups allowed to work with config +ALLOW_USERS="" +ALLOW_GROUPS="adm" + +# sync users and groups from ALLOW_USERS and ALLOW_GROUPS to .snapshots +# directory +SYNC_ACL="yes" + + +# start comparing pre- and post-snapshot in background after creating +# post-snapshot +BACKGROUND_COMPARISON="yes" + + +# run daily number cleanup +NUMBER_CLEANUP="no" + +# limit for number cleanup +NUMBER_MIN_AGE="1800" +NUMBER_LIMIT="50" +NUMBER_LIMIT_IMPORTANT="10" + +# "no": we will use systemd.timer +TIMELINE_CREATE="no" + +# create cron based cleanup entries +# "no": we will use systemd.timer +TIMELINE_CLEANUP="no" + +# snap-sync: timeline settings +TIMELINE_MIN_AGE="1800" +TIMELINE_LIMIT_HOURLY="1" +TIMELINE_LIMIT_DAILY="2" +TIMELINE_LIMIT_MONTHLY="1" +TIMELINE_LIMIT_YEARLY="1" + + +# cleanup empty pre-post-pairs +EMPTY_PRE_POST_CLEANUP="yes" + +# limits for empty pre-post-pair cleanup +EMPTY_PRE_POST_MIN_AGE="1800" + +# uncomment to exclude this subvol when calling +# snap-sync as timer unit +# SNAP_SUNC_EXCLUDE="yes" + diff --git a/src/find_snapper_config b/src/find_snapper_config new file mode 100755 index 0000000..746e741 --- /dev/null +++ b/src/find_snapper_config @@ -0,0 +1,15 @@ +#!/bin/sh + +etcdirs="sysconfig default conf.d" + +for x in $etcdirs; do + d=/etc/$x/snapper + if [ -f $d ]; then + sed -i 's@^SNAPPER_CONFIG=.*@SNAPPER_CONFIG='$d'@g' bin/dsnap-sync + exit 0 + fi +done + +printf "==> Unable to find snapper configuration file in a standard location.\n" +printf "==> Using SNAPPER_CONFIG make variable.\n" +exit 1