pax_global_header00006660000000000000000000000064132336063030014511gustar00rootroot0000000000000052 comment=56d77805a1985befebee21c2e0556b537e5e7dd5 snapper-0.5.4/000077500000000000000000000000001323360630300131675ustar00rootroot00000000000000snapper-0.5.4/.gitignore000066400000000000000000000003131323360630300151540ustar00rootroot00000000000000Makefile.in Makefile .deps .libs /.obsdir/ aclocal.m4 autom4te.cache config.* configure depcomp /debian install-sh libtool ltmain.sh missing obs-package-from-git stamp-h1 py-compile test-driver /compile snapper-0.5.4/.travis.debian.sh000077700000000000000000000000001323360630300215702.travis.ubuntu.shustar00rootroot00000000000000snapper-0.5.4/.travis.fedora.sh000077500000000000000000000016561323360630300163630ustar00rootroot00000000000000#! /bin/bash set -e -x make -f Makefile.repo make package # Build the binary package locally, use plain "rpmbuild" to make it simple. # "osc build" is too resource hungry (builds a complete chroot from scratch). # Moreover it does not work in a Docker container (it fails when trying to mount # /proc and /sys in the chroot). mkdir -p /root/rpmbuild/SOURCES cp package/* /root/rpmbuild/SOURCES rpmbuild -bb -D "fedora_version 25" -D "jobs `nproc`" package/*.spec # test the %pre/%post scripts by installing/updating/removing the built packages # ignore the dependencies to make the test easier, as a smoke test it's good enough rpm -iv --force --nodeps /root/rpmbuild/RPMS/*/*.rpm # smoke test, make sure snapper at least starts snapper --version rpm -Uv --force --nodeps /root/rpmbuild/RPMS/*/*.rpm # get the plain package names and remove all packages at once rpm -ev --nodeps `rpm -q --qf '%{NAME} ' -p /root/rpmbuild/RPMS/**/*.rpm` snapper-0.5.4/.travis.leap.sh000077700000000000000000000000001323360630300221022.travis.tumbleweed.shustar00rootroot00000000000000snapper-0.5.4/.travis.tumbleweed.sh000077500000000000000000000020021323360630300172420ustar00rootroot00000000000000#! /bin/bash set -e -x make -f Makefile.repo make package # run the osc source validator to check the .spec and .changes locally (cd package && /usr/lib/obs/service/source_validator) # Build the binary package locally, use plain "rpmbuild" to make it simple. # "osc build" is too resource hungry (builds a complete chroot from scratch). # Moreover it does not work in a Docker container (it fails when trying to mount # /proc and /sys in the chroot). cp package/* /usr/src/packages/SOURCES/ rpmbuild -bb -D "jobs `nproc`" package/*.spec # test the %pre/%post scripts by installing/updating/removing the built packages # ignore the dependencies to make the test easier, as a smoke test it's good enough rpm -iv --force --nodeps /usr/src/packages/RPMS/*/*.rpm # smoke test, make sure snapper at least starts snapper --version rpm -Uv --force --nodeps /usr/src/packages/RPMS/*/*.rpm # get the plain package names and remove all packages at once rpm -ev --nodeps `rpm -q --qf '%{NAME} ' -p /usr/src/packages/RPMS/**/*.rpm` snapper-0.5.4/.travis.ubuntu.sh000077500000000000000000000004561323360630300164420ustar00rootroot00000000000000#! /bin/bash set -e -x make -f Makefile.repo # copy the Debian files to the expected place cp -a dists/debian debian # build binary packages dpkg-buildpackage -j`nproc` -rfakeroot -b # install the built packages dpkg -i ../*.deb # smoke test, make sure snapper at least starts snapper --version snapper-0.5.4/.travis.yml000066400000000000000000000007211323360630300153000ustar00rootroot00000000000000sudo: required language: bash services: - docker # define the build matrix env: - BUILD_FLAVOR=tumbleweed - BUILD_FLAVOR=leap - BUILD_FLAVOR=fedora - BUILD_FLAVOR=ubuntu - BUILD_FLAVOR=debian before_install: # use the Dockerfile. file for building the image - docker build -f Dockerfile.$BUILD_FLAVOR -t snapper-devel . script: # run the respective .travis..sh script - docker run -it snapper-devel ./.travis.$BUILD_FLAVOR.sh snapper-0.5.4/AUTHORS000066400000000000000000000000421323360630300142330ustar00rootroot00000000000000Arvin Schnell snapper-0.5.4/COPYING000066400000000000000000000431031323360630300142230ustar00rootroot00000000000000 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. Copyright (C) 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. , 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. snapper-0.5.4/Dockerfile.debian000066400000000000000000000011401323360630300163760ustar00rootroot00000000000000# Build Debian 9 image FROM debian:9 # see https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/run RUN apt-get update && \ apt-get install -y --no-install-recommends \ acl-dev \ autoconf \ automake \ build-essential \ debhelper \ devscripts \ docbook-xsl \ fakeroot \ g++ \ gettext \ libboost-dev \ libboost-system-dev \ libboost-test-dev \ libboost-thread-dev \ libdbus-1-dev \ libmount-dev \ libpam-dev \ libtool \ libxml2-dev \ libz-dev \ locales-all \ xsltproc RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY . /usr/src/app snapper-0.5.4/Dockerfile.fedora000066400000000000000000000006501323360630300164210ustar00rootroot00000000000000# Build Fedora 27 image FROM fedora:27 RUN dnf -y install \ autoconf \ automake \ boost-devel \ dbus-devel \ docbook-style-xsl \ e2fsprogs-devel \ gcc-c++ \ gettext \ glibc-langpack-de \ glibc-langpack-en \ libacl-devel \ libmount-devel \ libtool \ libxml2-devel \ libxslt \ make \ pam-devel \ pkgconfig \ rpm-build RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY . /usr/src/app snapper-0.5.4/Dockerfile.leap000066400000000000000000000007371323360630300161100ustar00rootroot00000000000000# Build the latest openSUSE Leap (42.x) image FROM opensuse:leap RUN zypper --non-interactive in --no-recommends \ autoconf \ automake \ boost-devel \ dbus-1-devel \ docbook-xsl-stylesheets \ e2fsprogs-devel \ gcc-c++ \ grep \ libacl-devel \ libbtrfs-devel \ libmount-devel \ libtool \ libxml2-devel \ libxslt \ obs-service-source_validator \ pam-devel \ rpm-build \ which RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY . /usr/src/app snapper-0.5.4/Dockerfile.tumbleweed000066400000000000000000000010401323360630300173100ustar00rootroot00000000000000# Build the latest openSUSE Tumbleweed image FROM opensuse:tumbleweed RUN zypper --non-interactive in --no-recommends \ autoconf \ automake \ dbus-1-devel \ docbook-xsl-stylesheets \ e2fsprogs-devel \ gcc-c++ \ grep \ libacl-devel \ libboost_system-devel \ libboost_test-devel \ libboost_thread-devel \ libbtrfs-devel \ libmount-devel \ libtool \ libxml2-devel \ libxslt \ obs-service-source_validator \ pam-devel \ rpm-build \ which RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY . /usr/src/app snapper-0.5.4/Dockerfile.ubuntu000066400000000000000000000012021323360630300164750ustar00rootroot00000000000000# Build Ubuntu 17.10 image FROM ubuntu:17.10 # see https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/run RUN apt-get update && \ apt-get install -y --no-install-recommends \ acl-dev \ autoconf \ automake \ build-essential \ debhelper \ devscripts \ docbook-xsl \ fakeroot \ g++ \ gettext \ language-pack-de \ language-pack-en \ libboost-dev \ libboost-system-dev \ libboost-test-dev \ libboost-thread-dev \ libdbus-1-dev \ libmount-dev \ libpam-dev \ libtool \ libxml2-dev \ libz-dev \ xsltproc RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY . /usr/src/app snapper-0.5.4/LIBVERSION000066400000000000000000000000061323360630300145620ustar00rootroot000000000000004.1.0 snapper-0.5.4/Makefile.am000066400000000000000000000067421323360630300152340ustar00rootroot00000000000000# # Makefile.am for snapper # SUBDIRS = snapper examples dbus server client scripts pam data doc po \ testsuite testsuite-real testsuite-cmp AUTOMAKE_OPTIONS = foreign dist-bzip2 no-dist-gzip doc_DATA = AUTHORS COPYING EXTRA_DIST = $(doc_DATA) VERSION LIBVERSION snapper-$(VERSION).tar.bz2: dist-bzip2 DEBIAN_FLAVOURS = \ Debian_7.0 \ Debian_8.0 \ Debian_9.0 UBUNTU_FLAVOURS = \ xUbuntu_14.04 \ xUbuntu_14.10 \ xUbuntu_15.04 \ xUbuntu_15.10 \ xUbuntu_16.04 \ xUbuntu_16.10 \ xUbuntu_17.04 \ xUbuntu_17.10 show-debian: @echo "Debian flavors: $(DEBIAN_FLAVOURS)" show-ubuntu: @echo "Ubuntu flavors: $(UBUNTU_FLAVOURS)" package-clean: rm -f package/snapper-*.tar.bz2 rm -f package/debian.* rm -f package/*.dsc* # Create all the files necessary for building the package with OBS: # # - Clean up the package/ directory # - create a new tarball (via the depencency) # - copy the content of the debian/ directory # - for both Debian and Ubuntu, generate a master .dsc.in from file with the # "Files:" line for the tarball with its md5sum, file size in bytes, and name # - copy that .dsc.in master file for each flavor of Debian or Ubuntu to be supported # - remove the .dsc.in master file and the .dsc.in.in file # - move the new tarball to the package/ directory # # Unfortunately, using variables for the md5sum and the file size didn't work out # (not even with the GNU make ':=' syntax): They cannot be assigned in the 'actions' # part of a rule, only outside rules. # # The .dsc files are generated from a .dsc.in file for each Debian and Ubuntu, # which in turn are generated by configure/autoconf from .dsc.in.in files (see # configure.ac) where @VERSION@ is expanded with the content of the toplevel # VERSION file. # # $< is the first depencency of the rule, i.e. snapper-$(VERSION).tar.gz in this case. # Build a reproducible tarball: # - set the file time stamps according to the latest commit # - sort the files (in a locale independent way, use the NULL separator to # correctly process also the file names containing a new line) # Note: tar >= 1.28 supports "--sort=name" option, unfortunately # Leap 42.3 and SLES12-SP3 contain version 1.27.1 # - use the GNU format (the default POSIX format contains some time stamps) # - set the owner and group to "root" # - set the fixed modification time # shared tar options EXTRA_TAR_OPTIONS = --format=gnu --owner=root --group=root \ --mtime='$(shell git show -s --format=%ci)' --null --files-from - # redefine the standard automake "tar" command am__tar=find "$$tardir" -type f -print0 | LC_ALL=C sort -z | \ tar -c -f - $(EXTRA_TAR_OPTIONS) package: snapper-$(VERSION).tar.bz2 package-clean find dists/debian -not -name '*.in' -not -name '.*' -type f -print0 | \ LC_ALL=C sort -z | \ tar -c -f package/debian.tar --transform='s|dists/||' --show-transformed \ $(EXTRA_TAR_OPTIONS) ## use -n option to exclude the original file time stamps to have a reproducible tarball gzip -n package/debian.tar cp dists/debian/*.dsc.in package/ echo "$(shell md5sum $< | sed -e 's/\s.*//') $(shell wc -c $<)" >> package/snapper-Debian.dsc.in echo "$(shell md5sum $< | sed -e 's/\s.*//') $(shell wc -c $<)" >> package/snapper-xUbuntu.dsc.in for FLAV in $(DEBIAN_FLAVOURS); do cp -v package/snapper-Debian.dsc.in package/snapper-$${FLAV}.dsc; done for FLAV in $(UBUNTU_FLAVOURS); do cp -v package/snapper-xUbuntu.dsc.in package/snapper-$${FLAV}.dsc; done rm package/snapper*.dsc.in* mv snapper-$(VERSION).tar.bz2 package/ snapper-0.5.4/Makefile.ci000066400000000000000000000010111323360630300152120ustar00rootroot00000000000000# -*- Makefile -*- # Usage in Jenkins: # make -f Makefile.ci package .PHONY: package always_out_of_date package: obs-package-from-git ./$^ \ -P filesystems:snapper -p snapper \ -o .obsdir \ -c 'make -f Makefile.repo && make package' # A script that is shared with other projects and should be always refreshed URL=https://raw.githubusercontent.com/openSUSE/obs-package-from-git/master/obs-package-from-git .PHONY: always_out_of_date obs-package-from-git: always_out_of_date wget -O $@ $(URL) && chmod +x $@ snapper-0.5.4/Makefile.repo000066400000000000000000000006701323360630300155760ustar00rootroot00000000000000# # Makefile.repo for snapper # # somehow detect if this is a lib or lib64 system LIB = $(shell gcc -v 2>&1 | sed -n 's,.*--libdir=/usr/\([^ ]*\).*,\1,p') ifeq ($(LIB),) LIB = lib endif PREFIX = /usr configure: all ./configure --prefix=$(PREFIX) --libdir=$(PREFIX)/$(LIB) all: aclocal autoconf autoheader autoreconf --force --install install: configure make make install reconf: all ./config.status --recheck ./config.status snapper-0.5.4/README.Travis.md000066400000000000000000000027651323360630300157270ustar00rootroot00000000000000# Travis CI By default Travis uses Ubuntu 14.04 LTS for building, but it is possible to run the build in any Linux distribution using [Docker](https://www.docker.com/). ## Setup For each target distribution there is a `Dockerfile` and the respective `.travis.sh` script. The `Dockerfile` defines the steps needed for building the Docker image, the script runs the build in the specific distribution. The `.travis.yml` file defines the build matrix which runs builds for all configured distributions in parallel. It is defined in the `env` section, see more details in the [Travis documentation]( https://docs.travis-ci.com/user/customizing-the-build#Build-Matrix). ## Running the Build Locally - [Install and start Docker](https://docs.docker.com/engine/installation/linux/) - Run (as `root`) the same commands as in the `.travis.yml` file, that means: - First build the docker image locally, e.g. `docker build -f Dockerfile.tumbleweed -t snapper-devel .`, the Docker image automatically includes also the copy of the current Snapper sources. - Then run the build: `docker run -it --rm snapper-devel ./.travis.tumbleweed.sh` (The `--rm` will cleanup the new Docker image layer created by the build, if you want to inspect the build artifacts then remove it.) - If you need to debug a failure then run `bash` instead of the Travis script and run the build steps manually. If you need an editor or some other tool you can install them via the respective packaging tool, see the `Dockerfile` for examples. snapper-0.5.4/README.md000066400000000000000000000074641323360630300144610ustar00rootroot00000000000000 Snapper ======= [![Build Status](https://travis-ci.org/openSUSE/snapper.svg?branch=master)](https://travis-ci.org/openSUSE/snapper) Snapper is a tool for Linux file system snapshot management. Apart from the obvious creation and deletion of snapshots it can compare snapshots and revert differences between them. In simple terms, this allows root and non-root users to view older versions of files and revert changes. For more information visit [snapper.io](http://snapper.io/). Development ----------- For compiling and developing Snapper you need to setup the development environment first. ### Development Environment In the SUSE Linux Enterprise and openSUSE distributions you can install the needed packages by using these commands: ```sh # install the basic development environment (SUSE Linux Enterprise, the SDK extension is needed) sudo zypper install -t pattern SDK-C-C++ # install the basic development environment (openSUSE) sudo zypper install -t pattern devel_C_C++ # install the extra packages for snapper development (both SLE and openSUSE) sudo zypper install git libmount-devel dbus-1-devel libacl-devel \ docbook-xsl-stylesheets libxml2-devel libbtrfs-devel ``` Alternatively you can use a [Docker](https://www.docker.com/) container, see [REAME.Travis.md](REAME.Travis.md) file for some hints. ### Building Snapper You can download the sources and build Snapper by using these commands: ```sh git clone git@github.com:/snapper.git cd snapper make -f Makefile.repo # parallelize the build using more processors, use plain `make` if it does not work make -j`nproc` ``` ### Installing and Running Snapper To run the freshly built Snapper use this: ```sh sudo make install # kill the currently running DBus process if present sudo killall snapperd # try your changes (the DBus service is started automatically) (sudo) snapper ... ``` ### Running Tests Snapper includes some internal unit tests to avoid some bugs and regressions. The tests are located in the `testsuite` subdirectory and you can start them using the `make check` command. There are also some additional tests in the `testsuite-real` subdirectory, but be careful. *These tests really execute snapper commands and they can destroy your data! Run these tests only in a testing environment!* ### Releasing - Before releasing the Snapper package ensure that the changes made to the package are mentioned in the `package/snapper.changes` file, update also the `dists/debian/changelog` file. - Make sure the units tests still passes ([see above](#running-tests)). - When the version is increased then the Git repo has to be tagged, use the `vX.Y.Z` format for the tag. Also the [filesystems:snapper](https://build.opensuse.org/project/show/filesystems:snapper) OBS project has to be updated. - To create the package use command `make package`. Then use the common work-flow to submit package to the build service. For [openSUSE:Factory](https://build.opensuse.org/project/show/openSUSE:Factory) send at first the package to the devel project [YaST:Head](https://build.opensuse.org/project/show/YaST:Head) in OBS. *Please note that this OBS project builds for more distributions so more metadata files have to be updated. See the OBS documentation for more info ([cross distribution howto](https://en.opensuse.org/openSUSE:Build_Service_cross_distribution_howto), [Debian builds](https://en.opensuse.org/openSUSE:Build_Service_Debian_builds)).* - The generated bzip2 tarball has to be also placed at [ftp.suse.com/pub/projects/snapper](ftp://ftp.suse.com/pub/projects/snapper). - When the documentation changes e.g. the man page or an important functionality then also the [snapper.io](http://snapper.io/) web pages have to be updated. They are hosted as GitHub pages in the [gh-pages branch](https://github.com/openSUSE/snapper/tree/gh-pages) in the Snapper Git repository. snapper-0.5.4/Rakefile000066400000000000000000000027231323360630300146400ustar00rootroot00000000000000# -*- coding: utf-8 -*- # make continuous integration using rubygem-packaging_rake_tasks # Copyright © 2015 SUSE # MIT license require "packaging/tasks" require "packaging/configuration" # skip 'tarball' task, it's redefined here Packaging::Tasks.load_tasks(:exclude => ["tarball.rake"]) require "yast/tasks" yast_submit = ENV.fetch("YAST_SUBMIT", "factory").to_sym Yast::Tasks.submit_to(yast_submit) Packaging.configuration do |conf| conf.package_name.sub!(/-.*/, "") # strip branch name conf.package_dir = ".obsdir" # Makefile.ci puts it there conf.skip_license_check << /.*/ # defined in Rakefile in https://github.com/openSUSE/packaging_rake_tasks if yast_submit == :factory # Override values from # https://github.com/yast/yast-rake/blob/master/data/targets.yml # loaded by Yast::Tasks.submit_to() for OBS: # filesystems:snapper/snapper conf.obs_api = "https://api.opensuse.org/" conf.obs_project = "filesystems:snapper" conf.obs_sr_project = "openSUSE:Factory" conf.obs_target = "openSUSE_Factory" end end desc 'Show configuration' task :show_config do Packaging.configuration do |conf| puts "API: #{conf.obs_api}" puts "Project: #{conf.obs_project}" puts "SR Project: #{conf.obs_sr_project}" puts "Target: #{conf.obs_target}" end end desc 'Pretend to run the test suite' task :test do puts 'No tests yet' if verbose end desc 'Build a tarball for OBS' task :tarball do sh "make -f Makefile.ci package" end snapper-0.5.4/VERSION000066400000000000000000000000061323360630300142330ustar00rootroot000000000000000.5.4 snapper-0.5.4/client/000077500000000000000000000000001323360630300144455ustar00rootroot00000000000000snapper-0.5.4/client/.gitignore000066400000000000000000000000731323360630300164350ustar00rootroot00000000000000*.o snapper systemd-helper installation-helper mksubvolume snapper-0.5.4/client/Makefile.am000066400000000000000000000022641323360630300165050ustar00rootroot00000000000000# # Makefile.am for snapper/client # SUBDIRS = utils AM_CPPFLAGS = -I$(top_srcdir) $(DBUS_CFLAGS) bin_PROGRAMS = snapper snapper_SOURCES = \ snapper.cc \ types.cc types.h \ commands.cc commands.h \ cleanup.cc cleanup.h \ proxy.cc proxy.h \ proxy-dbus.cc proxy-dbus.h \ proxy-lib.cc proxy-lib.h \ misc.cc misc.h \ errors.cc errors.h snapper_LDADD = ../snapper/libsnapper.la utils/libutils.la ../dbus/libdbus.la libexecdir = /usr/lib/snapper libexec_PROGRAMS = systemd-helper systemd_helper_SOURCES = \ systemd-helper.cc \ types.cc types.h \ commands.cc commands.h \ cleanup.cc cleanup.h \ proxy.cc proxy.h \ proxy-dbus.cc proxy-dbus.h \ proxy-lib.cc proxy-lib.h \ misc.cc misc.h \ errors.cc errors.h systemd_helper_LDADD = ../snapper/libsnapper.la utils/libutils.la ../dbus/libdbus.la if ENABLE_BTRFS libexec_PROGRAMS += installation-helper installation_helper_SOURCES = \ installation-helper.cc \ misc.cc misc.h installation_helper_LDADD = ../snapper/libsnapper.la utils/libutils.la if ENABLE_ROLLBACK sbin_PROGRAMS = mksubvolume mksubvolume_SOURCES = \ mksubvolume.cc mksubvolume_LDADD = ../snapper/libsnapper.la utils/libutils.la endif endif snapper-0.5.4/client/cleanup.cc000066400000000000000000000365011323360630300164100ustar00rootroot00000000000000/* * Copyright (c) [2011-2014] Novell, Inc. * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include #include "dbus/DBusMessage.h" #include "dbus/DBusConnection.h" #include "snapper/SnapperTmpl.h" #include "snapper/Snapper.h" #include "utils/Range.h" #include "utils/equal-date.h" #include "cleanup.h" using namespace std; struct Parameters { Parameters(const ProxySnapper* snapper); virtual ~Parameters() {} virtual bool is_degenerated() const { return true; } time_t min_age; double space_limit; template void read(const ProxyConfig& config, const char* name, Type& value) { const map& raw = config.getAllValues(); map::const_iterator pos = raw.find(name); if (pos != raw.end()) pos->second >> value; } }; Parameters::Parameters(const ProxySnapper* snapper) : min_age(1800), space_limit(0.5) { ProxyConfig config = snapper->getConfig(); read(config, "SPACE_LIMIT", space_limit); } ostream& operator<<(ostream& s, const Parameters& parameters) { return s << "min-age:" << parameters.min_age << endl << "space-limit:" << parameters.space_limit; } class Cleaner { public: Cleaner(ProxySnapper* snapper, bool verbose, const Parameters& parameters) : snapper(snapper), verbose(verbose), parameters(parameters) {} virtual ~Cleaner() {} void cleanup(); protected: virtual list calculate_candidates(ProxySnapshots& snapshots, Range::Value value) = 0; struct younger_than { younger_than(time_t t) : t(t) {} bool operator()(ProxySnapshots::const_iterator it) { return it->getDate() > t; } const time_t t; }; void filter(ProxySnapshots& snapshots, list& tmp) const; // Removes snapshots younger than parameters.min_age from tmp void filter_min_age(ProxySnapshots& snapshots, list& tmp) const; // Removes pre and post snapshots from tmp that do have a corresponding // snapshot but which is not included in tmp. void filter_pre_post(ProxySnapshots& snapshots, list& tmp) const; void remove(const list& tmp); bool is_quota_aware() const; bool is_quota_satisfied() const; void cleanup_quota_unaware(ProxySnapshots& snapshots); void cleanup_quota_aware(ProxySnapshots& snapshots); ProxySnapper* snapper; bool verbose; const Parameters& parameters; }; void Cleaner::filter(ProxySnapshots& snapshots, list& tmp) const { filter_min_age(snapshots, tmp); filter_pre_post(snapshots, tmp); } void Cleaner::filter_min_age(ProxySnapshots& snapshots, list& tmp) const { time_t now = time(NULL); tmp.remove_if(younger_than(now - parameters.min_age)); } void Cleaner::filter_pre_post(ProxySnapshots& snapshots, list& tmp) const { list ret; for (list::iterator it1 = tmp.begin(); it1 != tmp.end(); ++it1) { if ((*it1)->getType() == PRE) { ProxySnapshots::const_iterator it2 = snapshots.findPost(*it1); if (it2 != snapshots.end()) { if (find(tmp.begin(), tmp.end(), it2) == tmp.end()) continue; } } if ((*it1)->getType() == POST) { ProxySnapshots::const_iterator it2 = snapshots.findPre(*it1); if (it2 != snapshots.end()) { if (find(tmp.begin(), tmp.end(), it2) == tmp.end()) continue; } } ret.push_back(*it1); } swap(ret, tmp); } void Cleaner::remove(const list& tmp) { for (list::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { snapper->deleteSnapshots({ *it }, verbose); } } bool Cleaner::is_quota_aware() const { if (parameters.is_degenerated()) return false; try { snapper->prepareQuota(); } catch (const DBus::ErrorException& e) { SN_CAUGHT(e); if (strcmp(e.name(), "error.quota") == 0) { cerr << "quota not working (" << e.message() << ")" << endl; return false; } SN_RETHROW(e); } catch (const QuotaException& e) { SN_CAUGHT(e); cerr << "quota not working (" << e.what() << ")" << endl; return false; } return true; } bool Cleaner::is_quota_satisfied() const { QuotaData quota_data = snapper->queryQuotaData(); return quota_data.used < parameters.space_limit * quota_data.size; } void Cleaner::cleanup_quota_unaware(ProxySnapshots& snapshots) { list candidates = calculate_candidates(snapshots, Range::MAX); filter(snapshots, candidates); remove(candidates); } void Cleaner::cleanup_quota_aware(ProxySnapshots& snapshots) { while (!is_quota_satisfied()) { list candidates = calculate_candidates(snapshots, Range::MIN); if (candidates.empty()) { // not enough candidates to satisfy quota return; } // take more and more candidates into a temporary candidates // list. this is required since the filter will e.g. remove a pre // snapshot candidate if the post snapshot is missing so simply // removing the first candidate is not possible. for (list::iterator e = candidates.begin(); e != candidates.end(); ++e) { list tmp = list(candidates.begin(), next(e)); filter(snapshots, tmp); if (!tmp.empty()) { remove(tmp); // after removing snapshots is_quota_satisfied must be reevaluated break; } if (next(e) == candidates.end()) { // not enough candidates to satisfy quota return; } } } } void Cleaner::cleanup() { ProxySnapshots& snapshots = snapper->getSnapshots(); cleanup_quota_unaware(snapshots); if (is_quota_aware()) cleanup_quota_aware(snapshots); } struct NumberParameters : public Parameters { NumberParameters(const ProxySnapper* snapper); bool is_degenerated() const; Range limit; Range limit_important; }; NumberParameters::NumberParameters(const ProxySnapper* snapper) : Parameters(snapper), limit(50), limit_important(10) { ProxyConfig config = snapper->getConfig(); read(config, "NUMBER_MIN_AGE", min_age); read(config, "NUMBER_LIMIT", limit); read(config, "NUMBER_LIMIT_IMPORTANT", limit_important); } ostream& operator<<(ostream& s, const NumberParameters& parameters) { return s << dynamic_cast(parameters) << endl << "limit:" << parameters.limit << endl << "limit-important:" << parameters.limit_important; } bool NumberParameters::is_degenerated() const { return limit.is_degenerated() && limit_important.is_degenerated(); } class NumberCleaner : public Cleaner { public: NumberCleaner(ProxySnapper* snapper, bool verbose, const NumberParameters& parameters) : Cleaner(snapper, verbose, parameters) {} private: bool is_important(ProxySnapshots::const_iterator it1) { map::const_iterator it2 = it1->getUserdata().find("important"); return it2 != it1->getUserdata().end() && it2->second == "yes"; } list calculate_candidates(ProxySnapshots& snapshots, Range::Value value) override { const NumberParameters& parameters = dynamic_cast(Cleaner::parameters); list ret; for (ProxySnapshots::iterator it = snapshots.begin(); it != snapshots.end(); ++it) { if (it->getCleanup() == "number") ret.push_front(it); } size_t num = 0; size_t num_important = 0; list::iterator it = ret.begin(); while (it != ret.end()) { bool keep = false; if (num_important < parameters.limit_important.value(value) && is_important(*it)) { ++num_important; keep = true; } if (num < parameters.limit.value(value)) { ++num; keep = true; } if (keep) it = ret.erase(it); else ++it; } ret.reverse(); return ret; } }; void do_cleanup_number(ProxySnapper* snapper, bool verbose) { NumberParameters parameters(snapper); NumberCleaner cleaner(snapper, verbose, parameters); cleaner.cleanup(); } struct TimelineParameters : public Parameters { TimelineParameters(const ProxySnapper* snapper); bool is_degenerated() const; Range limit_hourly; Range limit_daily; Range limit_monthly; Range limit_weekly; Range limit_yearly; }; TimelineParameters::TimelineParameters(const ProxySnapper* snapper) : Parameters(snapper), limit_hourly(10), limit_daily(10), limit_monthly(10), limit_weekly(0), limit_yearly(10) { ProxyConfig config = snapper->getConfig(); read(config, "TIMELINE_MIN_AGE", min_age); read(config, "TIMELINE_LIMIT_HOURLY", limit_hourly); read(config, "TIMELINE_LIMIT_DAILY", limit_daily); read(config, "TIMELINE_LIMIT_WEEKLY", limit_weekly); read(config, "TIMELINE_LIMIT_MONTHLY", limit_monthly); read(config, "TIMELINE_LIMIT_YEARLY", limit_yearly); } ostream& operator<<(ostream& s, const TimelineParameters& parameters) { return s << dynamic_cast(parameters) << endl << "limit-hourly:" << parameters.limit_hourly << endl << "limit-daily:" << parameters.limit_daily << endl << "limit-weekly:" << parameters.limit_weekly << endl << "limit-monthly:" << parameters.limit_monthly << endl << "limit-yearly:" << parameters.limit_yearly; } bool TimelineParameters::is_degenerated() const { return limit_hourly.is_degenerated() && limit_daily.is_degenerated() && limit_monthly.is_degenerated() && limit_weekly.is_degenerated() && limit_yearly.is_degenerated(); } class TimelineCleaner : public Cleaner { public: TimelineCleaner(ProxySnapper* snapper, bool verbose, const TimelineParameters& parameters) : Cleaner(snapper, verbose, parameters) {} private: bool is_first(list::const_iterator first, list::const_iterator last, ProxySnapshots::const_iterator it1, std::function pred) { time_t t1 = it1->getDate(); struct tm tmp1; localtime_r(&t1, &tmp1); for (list::const_iterator it2 = first; it2 != last; ++it2) { if (it1 == *it2) continue; time_t t2 = (*it2)->getDate(); struct tm tmp2; localtime_r(&t2, &tmp2); if (!pred(tmp1, tmp2)) return true; if (t1 > t2) return false; } return true; } bool is_first_yearly(list::const_iterator first, list::const_iterator last, ProxySnapshots::const_iterator it1) { return is_first(first, last, it1, equal_year); } bool is_first_monthly(list::const_iterator first, list::const_iterator last, ProxySnapshots::const_iterator it1) { return is_first(first, last, it1, equal_month); } bool is_first_weekly(list::const_iterator first, list::const_iterator last, ProxySnapshots::const_iterator it1) { return is_first(first, last, it1, equal_week); } bool is_first_daily(list::const_iterator first, list::const_iterator last, ProxySnapshots::const_iterator it1) { return is_first(first, last, it1, equal_day); } bool is_first_hourly(list::const_iterator first, list::const_iterator last, ProxySnapshots::const_iterator it1) { return is_first(first, last, it1, equal_hour); } list calculate_candidates(ProxySnapshots& snapshots, Range::Value value) override { const TimelineParameters& parameters = dynamic_cast(Cleaner::parameters); list ret; for (ProxySnapshots::iterator it = snapshots.begin(); it != snapshots.end(); ++it) { if (it->getCleanup() == "timeline") ret.push_front(it); } size_t num_hourly = 0; size_t num_daily = 0; size_t num_weekly = 0; size_t num_monthly = 0; size_t num_yearly = 0; list::iterator it = ret.begin(); while (it != ret.end()) { bool keep = false; if (num_hourly < parameters.limit_hourly.value(value) && is_first_hourly(it, ret.end(), *it)) { ++num_hourly; keep = true; } if (num_daily < parameters.limit_daily.value(value) && is_first_daily(it, ret.end(), *it)) { ++num_daily; keep = true; } if (num_weekly < parameters.limit_weekly.value(value) && is_first_weekly(it, ret.end(), *it)) { ++num_weekly; keep = true; } if (num_monthly < parameters.limit_monthly.value(value) && is_first_monthly(it, ret.end(), *it)) { ++num_monthly; keep = true; } if (num_yearly < parameters.limit_yearly.value(value) && is_first_yearly(it, ret.end(), *it)) { ++num_yearly; keep = true; } if (keep) it = ret.erase(it); else ++it; } ret.reverse(); return ret; } }; void do_cleanup_timeline(ProxySnapper* snapper, bool verbose) { TimelineParameters parameters(snapper); TimelineCleaner cleaner(snapper, verbose, parameters); cleaner.cleanup(); } struct EmptyPrePostParameters : public Parameters { EmptyPrePostParameters(const ProxySnapper* snapper); }; EmptyPrePostParameters::EmptyPrePostParameters(const ProxySnapper* snapper) : Parameters(snapper) { ProxyConfig config = snapper->getConfig(); read(config, "EMPTY_PRE_POST_MIN_AGE", min_age); } class EmptyPrePostCleaner : public Cleaner { public: EmptyPrePostCleaner(ProxySnapper* snapper, bool verbose, const EmptyPrePostParameters& parameters) : Cleaner(snapper, verbose, parameters) {} private: list calculate_candidates(ProxySnapshots& snapshots, Range::Value value) override { list ret; for (ProxySnapshots::iterator it1 = snapshots.begin(); it1 != snapshots.end(); ++it1) { if (it1->getType() == PRE) { ProxySnapshots::iterator it2 = snapshots.findPost(it1); if (it2 != snapshots.end()) { ProxyComparison comparison = snapper->createComparison(*it1, *it2, false); if (comparison.getFiles().empty()) { ret.push_back(it1); ret.push_back(it2); } } } } return ret; } }; void do_cleanup_empty_pre_post(ProxySnapper* snapper, bool verbose) { EmptyPrePostParameters parameters(snapper); EmptyPrePostCleaner cleaner(snapper, verbose, parameters); cleaner.cleanup(); } snapper-0.5.4/client/cleanup.h000066400000000000000000000020071323360630300162440ustar00rootroot00000000000000/* * Copyright (c) [2011-2012] Novell, Inc. * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include "proxy.h" void do_cleanup_number(ProxySnapper* snapper, bool verbose); void do_cleanup_timeline(ProxySnapper* snapper, bool verbose); void do_cleanup_empty_pre_post(ProxySnapper* snapper, bool verbose); snapper-0.5.4/client/commands.cc000066400000000000000000000260071323360630300165620ustar00rootroot00000000000000/* * Copyright (c) [2012-2015] Novell, Inc. * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include "commands.h" #include "utils/text.h" #include "snapper/AppUtil.h" using namespace std; #define SERVICE "org.opensuse.Snapper" #define OBJECT "/org/opensuse/Snapper" #define INTERFACE "org.opensuse.Snapper" vector command_list_xconfigs(DBus::Connection& conn) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "ListConfigs"); DBus::Message reply = conn.send_with_reply_and_block(call); vector ret; DBus::Hihi hihi(reply); hihi >> ret; return ret; } XConfigInfo command_get_xconfig(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "GetConfig"); DBus::Hoho hoho(call); hoho << config_name; DBus::Message reply = conn.send_with_reply_and_block(call); XConfigInfo ret; DBus::Hihi hihi(reply); hihi >> ret; return ret; } void command_set_xconfig(DBus::Connection& conn, const string& config_name, const map& raw) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "SetConfig"); DBus::Hoho hoho(call); hoho << config_name << raw; conn.send_with_reply_and_block(call); } void command_create_config(DBus::Connection& conn, const string& config_name, const string& subvolume, const string& fstype, const string& template_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreateConfig"); DBus::Hoho hoho(call); hoho << config_name << subvolume << fstype << template_name; conn.send_with_reply_and_block(call); } void command_delete_config(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "DeleteConfig"); DBus::Hoho hoho(call); hoho << config_name; conn.send_with_reply_and_block(call); } XSnapshots command_list_xsnapshots(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "ListSnapshots"); DBus::Hoho hoho(call); hoho << config_name; DBus::Message reply = conn.send_with_reply_and_block(call); XSnapshots ret; DBus::Hihi hihi(reply); hihi >> ret.entries; return ret; } XSnapshot command_get_xsnapshot(DBus::Connection& conn, const string& config_name, unsigned int num) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "GetSnapshot"); DBus::Hoho hoho(call); hoho << config_name << num; DBus::Message reply = conn.send_with_reply_and_block(call); XSnapshot ret; DBus::Hihi hihi(reply); hihi >> ret; return ret; } void command_set_snapshot(DBus::Connection& conn, const string& config_name, unsigned int num, const SMD& smd) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "SetSnapshot"); DBus::Hoho hoho(call); hoho << config_name << num << smd.description << smd.cleanup << smd.userdata; conn.send_with_reply_and_block(call); } unsigned int command_create_single_snapshot(DBus::Connection& conn, const string& config_name, const string& description, const string& cleanup, const map& userdata) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreateSingleSnapshot"); DBus::Hoho hoho(call); hoho << config_name << description << cleanup << userdata; DBus::Message reply = conn.send_with_reply_and_block(call); unsigned int number; DBus::Hihi hihi(reply); hihi >> number; return number; } unsigned int command_create_single_snapshot_v2(DBus::Connection& conn, const string& config_name, unsigned int parent_num, bool read_only, const string& description, const string& cleanup, const map& userdata) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreateSingleSnapshotV2"); DBus::Hoho hoho(call); hoho << config_name << parent_num << read_only << description << cleanup << userdata; DBus::Message reply = conn.send_with_reply_and_block(call); unsigned int number; DBus::Hihi hihi(reply); hihi >> number; return number; } unsigned int command_create_single_snapshot_of_default(DBus::Connection& conn, const string& config_name, bool read_only, const string& description, const string& cleanup, const map& userdata) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreateSingleSnapshotOfDefault"); DBus::Hoho hoho(call); hoho << config_name << read_only << description << cleanup << userdata; DBus::Message reply = conn.send_with_reply_and_block(call); unsigned int number; DBus::Hihi hihi(reply); hihi >> number; return number; } unsigned int command_create_pre_snapshot(DBus::Connection& conn, const string& config_name, const string& description, const string& cleanup, const map& userdata) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreatePreSnapshot"); DBus::Hoho hoho(call); hoho << config_name << description << cleanup << userdata; DBus::Message reply = conn.send_with_reply_and_block(call); unsigned int number; DBus::Hihi hihi(reply); hihi >> number; return number; } unsigned int command_create_post_snapshot(DBus::Connection& conn, const string& config_name, unsigned int prenum, const string& description, const string& cleanup, const map& userdata) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreatePostSnapshot"); DBus::Hoho hoho(call); hoho << config_name << prenum << description << cleanup << userdata; DBus::Message reply = conn.send_with_reply_and_block(call); unsigned int number; DBus::Hihi hihi(reply); hihi >> number; return number; } void command_delete_snapshots(DBus::Connection& conn, const string& config_name, const vector& nums, bool verbose) { if (verbose) { cout << sformat(_("Deleting snapshot from %s:", "Deleting snapshots from %s:", nums.size()), config_name.c_str()) << endl; for (vector::const_iterator it = nums.begin(); it != nums.end(); ++it) { if (it != nums.begin()) cout << ", "; cout << *it; } cout << endl; } DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "DeleteSnapshots"); DBus::Hoho hoho(call); hoho << config_name << nums; conn.send_with_reply_and_block(call); } string command_mount_snapshot(DBus::Connection& conn, const string& config_name, unsigned int num, bool user_request) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "MountSnapshot"); DBus::Hoho hoho(call); hoho << config_name << num << user_request; DBus::Message reply = conn.send_with_reply_and_block(call); string mount_point; DBus::Hihi hihi(reply); hihi >> mount_point; return mount_point; } void command_umount_snapshot(DBus::Connection& conn, const string& config_name, unsigned int num, bool user_request) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "UmountSnapshot"); DBus::Hoho hoho(call); hoho << config_name << num << user_request; conn.send_with_reply_and_block(call); } string command_get_mount_point(DBus::Connection& conn, const string& config_name, unsigned int num) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "GetMountPoint"); DBus::Hoho hoho(call); hoho << config_name << num; DBus::Message reply = conn.send_with_reply_and_block(call); string mount_point; DBus::Hihi hihi(reply); hihi >> mount_point; return mount_point; } void command_create_comparison(DBus::Connection& conn, const string& config_name, unsigned int number1, unsigned int number2) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "CreateComparison"); DBus::Hoho hoho(call); hoho << config_name << number1 << number2; conn.send_with_reply_and_block(call); } void command_delete_comparison(DBus::Connection& conn, const string& config_name, unsigned int number1, unsigned int number2) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "DeleteComparison"); DBus::Hoho hoho(call); hoho << config_name << number1 << number2; conn.send_with_reply_and_block(call); } int operator<(const XFile& lhs, const XFile& rhs) { return File::cmp_lt(lhs.name, rhs.name); } vector command_get_xfiles(DBus::Connection& conn, const string& config_name, unsigned int number1, unsigned int number2) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "GetFiles"); DBus::Hoho hoho(call); hoho << config_name << number1 << number2; DBus::Message reply = conn.send_with_reply_and_block(call); vector files; DBus::Hihi hihi(reply); hihi >> files; sort(files.begin(), files.end()); // snapperd can have different locale than client // so sorting is required here return files; } void command_setup_quota(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "SetupQuota"); DBus::Hoho hoho(call); hoho << config_name; conn.send_with_reply_and_block(call); } void command_prepare_quota(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "PrepareQuota"); DBus::Hoho hoho(call); hoho << config_name; conn.send_with_reply_and_block(call); } QuotaData command_query_quota(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "QueryQuota"); DBus::Hoho hoho(call); hoho << config_name; DBus::Message reply = conn.send_with_reply_and_block(call); QuotaData quota_data; DBus::Hihi hihi(reply); hihi >> quota_data; return quota_data; } void command_sync(DBus::Connection& conn, const string& config_name) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "Sync"); DBus::Hoho hoho(call); hoho << config_name; conn.send_with_reply_and_block(call); } vector command_debug(DBus::Connection& conn) { DBus::MessageMethodCall call(SERVICE, OBJECT, INTERFACE, "Debug"); DBus::Message reply = conn.send_with_reply_and_block(call); vector lines; DBus::Hihi hihi(reply); hihi >> lines; return lines; } snapper-0.5.4/client/commands.h000066400000000000000000000100311323360630300164120ustar00rootroot00000000000000/* * Copyright (c) [2012-2015] Novell, Inc. * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include #include using std::string; using std::vector; using std::map; #include "types.h" vector command_list_xconfigs(DBus::Connection& conn); XConfigInfo command_get_xconfig(DBus::Connection& conn, const string& config_name); void command_set_xconfig(DBus::Connection& conn, const string& config_name, const map& raw); void command_create_config(DBus::Connection& conn, const string& config_name, const string& subvolume, const string& fstype, const string& template_name); void command_delete_config(DBus::Connection& conn, const string& config_name); XSnapshots command_list_xsnapshots(DBus::Connection& conn, const string& config_name); XSnapshot command_get_xsnapshot(DBus::Connection& conn, const string& config_name, unsigned int num); void command_set_snapshot(DBus::Connection& conn, const string& config_name, unsigned int num, const SMD& smd); unsigned int command_create_single_snapshot(DBus::Connection& conn, const string& config_name, const string& description, const string& cleanup, const map& userdata); unsigned int command_create_single_snapshot_v2(DBus::Connection& conn, const string& config_name, unsigned int parent_num, bool read_only, const string& description, const string& cleanup, const map& userdata); unsigned int command_create_single_snapshot_of_default(DBus::Connection& conn, const string& config_name, bool read_only, const string& description, const string& cleanup, const map& userdata); unsigned int command_create_pre_snapshot(DBus::Connection& conn, const string& config_name, const string& description, const string& cleanup, const map& userdata); unsigned int command_create_post_snapshot(DBus::Connection& conn, const string& config_name, unsigned int prenum, const string& description, const string& cleanup, const map& userdata); void command_delete_snapshots(DBus::Connection& conn, const string& config_name, const vector& nums, bool verbose); string command_mount_snapshot(DBus::Connection& conn, const string& config_name, unsigned int num, bool user_request); void command_umount_snapshot(DBus::Connection& conn, const string& config_name, unsigned int num, bool user_request); string command_get_mount_point(DBus::Connection& conn, const string& config_name, unsigned int num); void command_create_comparison(DBus::Connection& conn, const string& config_name, unsigned int number1, unsigned int number2); void command_delete_comparison(DBus::Connection& conn, const string& config_name, unsigned int number1, unsigned int number2); vector command_get_xfiles(DBus::Connection& conn, const string& config_name, unsigned int number1, unsigned int number2); void command_setup_quota(DBus::Connection& conn, const string& config_name); void command_prepare_quota(DBus::Connection& conn, const string& config_name); QuotaData command_query_quota(DBus::Connection& conn, const string& config_name); void command_sync(DBus::Connection& conn, const string& config_name); vector command_debug(DBus::Connection& conn); snapper-0.5.4/client/errors.cc000066400000000000000000000046211323360630300162730ustar00rootroot00000000000000/* * Copyright (c) [2011-2014] Novell, Inc. * Copyright (c) [2016] SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include "utils/text.h" #include "errors.h" using namespace std; using namespace snapper; string error_description(const DBus::ErrorException& e) { string name = e.name(); if (name == "error.unknown_config") return _("Unknown config."); if (name == "error.no_permissions") return _("No permissions."); if (name == "error.invalid_userdata") return _("Invalid userdata."); if (name == "error.invalid_configdata") return _("Invalid configdata."); if (name == "error.illegal_snapshot") return _("Illegal snapshot."); if (name == "error.config_locked") return _("Config is locked."); if (name == "error.config_in_use") return _("Config is in use."); if (name == "error.snapshot_in_use") return _("Snapshot is in use."); if (name == "error.io_error") return sformat(_("IO Error (%s)."), e.message()); if (name == "error.create_config_failed") return sformat(_("Creating config failed (%s)."), e.message()); if (name == "error.delete_config_failed") return sformat(_("Deleting config failed (%s)."), e.message()); if (name == "error.create_snapshot_failed") return _("Creating snapshot failed."); if (name == "error.delete_snapshot_failed") return _("Deleting snapshot failed."); if (name == "error.invalid_user") return _("Invalid user."); if (name == "error.invalid_group") return _("Invalid group."); if (name == "error.acl_error") return _("ACL error."); if (name == "error.quota") return sformat(_("Quota failure (%s)."), e.message()); return sformat(_("Failure (%s)."), name.c_str()); } snapper-0.5.4/client/errors.h000066400000000000000000000016241323360630300161350ustar00rootroot00000000000000/* * Copyright (c) [2011-2014] Novell, Inc. * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include using std::string; #include "dbus/DBusConnection.h" string error_description(const DBus::ErrorException& e); snapper-0.5.4/client/installation-helper.cc000066400000000000000000000217261323360630300207420ustar00rootroot00000000000000/* * Copyright (c) 2015 Novell, Inc. * Copyright (c) 2018 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "snapper/Log.h" #include "utils/GetOpts.h" #include "misc.h" using namespace snapper; using namespace std; void step1(const string& device, const string& description, const string& cleanup, const map& userdata) { // step runs in inst-sys // preconditions (maybe incomplete): // device is formatted with btrfs // default subvolume for device is set to (for installation) final value cout << "step 1 device:" << device << endl; cout << "temporarily mounting device" << endl; SDir s_dir("/"); TmpMount tmp_mount(s_dir, device, "tmp-mnt-XXXXXX", "btrfs", 0, ""); cout << "copying/modifying config-file" << endl; mkdir((tmp_mount.getFullname() + "/etc").c_str(), 0777); mkdir((tmp_mount.getFullname() + "/etc/snapper").c_str(), 0777); mkdir((tmp_mount.getFullname() + "/etc/snapper/configs").c_str(), 0777); try { SysconfigFile config(CONFIGTEMPLATEDIR "/" "default"); config.setName(tmp_mount.getFullname() + CONFIGSDIR "/" "root"); config.setValue(KEY_SUBVOLUME, "/"); config.setValue(KEY_FSTYPE, "btrfs"); } catch (const FileNotFoundException& e) { cerr << "copying/modifying config-file failed" << endl; } cout << "creating filesystem config" << endl; Btrfs btrfs("/", tmp_mount.getFullname()); btrfs.createConfig(); cout << "creating subvolume" << endl; Snapper snapper("root", tmp_mount.getFullname()); SCD scd; scd.read_only = false; scd.empty = true; scd.description = description; scd.cleanup = cleanup; scd.userdata = userdata; Snapshots::iterator snapshot = snapper.createSingleSnapshot(scd); cout << "again copying config-file" << endl; string ris = tmp_mount.getFullname() + snapshot->snapshotDir(); mkdir((ris + "/etc").c_str(), 0777); mkdir((ris + "/etc/snapper").c_str(), 0777); mkdir((ris + "/etc/snapper/configs").c_str(), 0777); system(("/bin/cp " + tmp_mount.getFullname() + "/etc/snapper/configs/root " + ris + "/etc/snapper/configs").c_str()); cout << "setting default subvolume" << endl; snapper.getFilesystem()->setDefault(snapshot->getNum()); cout << "done" << endl; } void step2(const string& device, const string& root_prefix, const string& default_subvolume_name) { // step runs in inst-sys // preconditions (maybe incomplete): // default subvolume of device is mounted at root-prefix // .snapshots subvolume is not mounted cout << "step 2 device:" << device << " root-prefix:" << root_prefix << " default-subvolume-name:" << default_subvolume_name << endl; cout << "mounting device" << endl; // The btrfs subvol mount option is either "@/.snapshots" or just // ".snapshots" depending on whether default_subvolume_name is e.g. "@" or // "". string subvol_option = default_subvolume_name; if (!subvol_option.empty()) subvol_option += "/"; subvol_option += ".snapshots"; mkdir((root_prefix + "/.snapshots").c_str(), 0777); SDir s_dir(root_prefix + "/.snapshots"); if (!s_dir.mount(device, "btrfs", 0, "subvol=" + subvol_option)) { cerr << "mounting .snapshots failed" << endl; } cout << "done" << endl; } void step3(const string& root_prefix, const string& default_subvolume_name) { // step runs in inst-sys // preconditions (maybe incomplete): // default subvolume of device is mounted at root-prefix // fstab at root-prefix contains entry for default subvolume of device cout << "step 3 root-prefix:" << root_prefix << " default_subvolume_name:" << default_subvolume_name << endl; cout << "adding .snapshots to fstab" << endl; Btrfs btrfs("/", root_prefix); btrfs.addToFstab(default_subvolume_name); cout << "done" << endl; } void step4() { // step runs in chroot // preconditions (maybe incomplete): // snapper rpms installed in chroot // all programs for snapper hooks installed in chroot // all preconditions for hooks satisfied cout << "step 4" << endl; cout << "modifying sysconfig-file" << endl; try { SysconfigFile sysconfig(SYSCONFIGFILE); sysconfig.setValue("SNAPPER_CONFIGS", { "root" }); } catch (const FileNotFoundException& e) { cerr << "sysconfig-file not found" << endl; } Btrfs btrfs("/", ""); cout << "running external programs" << endl; Hooks::create_config("/", &btrfs); cout << "done" << endl; } bool step5(const string& root_prefix, const string& snapshot_type, unsigned int pre_num, const string& description, const string& cleanup, const map& userdata) { // fate #317973 // preconditions (maybe incomplete): // snapper rpms installed SCD scd; scd.read_only = true; scd.description = description; scd.cleanup = cleanup; scd.userdata = userdata; Snapper snapper("root", root_prefix); Snapshots::iterator snapshot; try { if (snapshot_type == "single") { snapshot = snapper.createSingleSnapshot(scd); } else if (snapshot_type == "pre") { snapshot = snapper.createPreSnapshot(scd); } else if (snapshot_type == "post") { Snapshots snapshots = snapper.getSnapshots(); Snapshots::iterator pre = snapshots.find(pre_num); snapshot = snapper.createPostSnapshot(pre, scd); } } catch (const runtime_error& e) { y2err("create snapshot failed, " << e.what()); return false; } cout << snapshot->getNum() << endl; return true; } void log_do(LogLevel level, const string& component, const char* file, const int line, const char* func, const string& text) { cerr << text << endl; } bool log_query(LogLevel level, const string& component) { return level == ERROR; } int main(int argc, char** argv) { setlocale(LC_ALL, ""); setLogDo(&log_do); setLogQuery(&log_query); const struct option options[] = { { "step", required_argument, 0, 0 }, { "device", required_argument, 0, 0 }, { "root-prefix", required_argument, 0, 0 }, { "default-subvolume-name", required_argument, 0, 0 }, { "snapshot-type", required_argument, 0, 0 }, { "pre-num", required_argument, 0, 0 }, { "description", required_argument, 0, 0 }, { "cleanup", required_argument, 0, 0 }, { "userdata", required_argument, 0, 0 }, { 0, 0, 0, 0 } }; string step; string device; string root_prefix = "/"; string default_subvolume_name; string snapshot_type = "single"; unsigned int pre_num = 0; string description; string cleanup; map userdata; GetOpts getopts; getopts.init(argc, argv); GetOpts::parsed_opts opts = getopts.parse(options); GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("step")) != opts.end()) step = opt->second; if ((opt = opts.find("device")) != opts.end()) device = opt->second; if ((opt = opts.find("root-prefix")) != opts.end()) root_prefix = opt->second; if ((opt = opts.find("default-subvolume-name")) != opts.end()) default_subvolume_name = opt->second; if ((opt = opts.find("snapshot-type")) != opts.end()) snapshot_type = opt->second; if ((opt = opts.find("pre-num")) != opts.end()) pre_num = read_num(opt->second); if ((opt = opts.find("description")) != opts.end()) description = opt->second; if ((opt = opts.find("cleanup")) != opts.end()) cleanup = opt->second; if ((opt = opts.find("userdata")) != opts.end()) userdata = read_userdata(opt->second); if (step == "1") step1(device, description, cleanup, userdata); else if (step == "2") step2(device, root_prefix, default_subvolume_name); else if (step == "3") step3(root_prefix, default_subvolume_name); else if (step == "4") step4(); else if (step == "5") return step5(root_prefix, snapshot_type, pre_num, description, cleanup, userdata) ? EXIT_SUCCESS : EXIT_FAILURE; } snapper-0.5.4/client/misc.cc000066400000000000000000000075771323360630300157270ustar00rootroot00000000000000/* * Copyright (c) [2011-2014] Novell, Inc. * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include "config.h" #include #include #include #include #include #include "utils/text.h" #include "misc.h" unsigned int read_num(const string& str) { istringstream s(str); unsigned int num = 0; s >> num; if (s.fail() || !s.eof()) { cerr << sformat(_("Invalid snapshot '%s'."), str.c_str()) << endl; exit(EXIT_FAILURE); } return num; } map read_userdata(const string& s, const map& old) { if (s.empty()) { cerr << _("Empty userdata.") << endl; exit(EXIT_FAILURE); } list tmp; boost::split(tmp, s, boost::is_any_of(","), boost::token_compress_on); if (tmp.empty()) { cerr << _("Empty userdata.") << endl; exit(EXIT_FAILURE); } map userdata = old; for (list::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { string::size_type pos = it->find("="); if (pos == string::npos) { cerr << sformat(_("Userdata '%s' does not include '=' sign."), it->c_str()) << endl; exit(EXIT_FAILURE); } string key = boost::trim_copy(it->substr(0, pos)); string value = boost::trim_copy(it->substr(pos + 1)); if (key.empty()) { cerr << sformat(_("Userdata '%s' has empty key."), it->c_str()) << endl; exit(EXIT_FAILURE); } if (value.empty()) userdata.erase(key); else userdata[key] = value; } return userdata; } string show_userdata(const map& userdata) { string s; for (map::const_iterator it = userdata.begin(); it != userdata.end(); ++it) { if (!s.empty()) s += ", "; s += it->first + "=" + it->second; } return s; } map read_configdata(const list& l, const map& old) { if (l.empty()) { cerr << _("Empty configdata.") << endl; exit(EXIT_FAILURE); } map configdata = old; for (list::const_iterator it = l.begin(); it != l.end(); ++it) { string::size_type pos = it->find("="); if (pos == string::npos) { cerr << sformat(_("Configdata '%s' does not include '=' sign."), it->c_str()) << endl; exit(EXIT_FAILURE); } string key = boost::trim_copy(it->substr(0, pos)); string value = boost::trim_copy(it->substr(pos + 1)); if (key.empty()) { cerr << sformat(_("Configdata '%s' has empty key."), it->c_str()) << endl; exit(EXIT_FAILURE); } configdata[key] = value; } return configdata; } string username(uid_t uid) { string username; gid_t gid; if (!get_uid_username_gid(uid, username, gid)) return sformat("unknown (%d)", uid); return username; } Differ::Differ() : command(DIFFBIN " --new-file --unified"), extensions() { } void Differ::run(const string& f1, const string& f2) const { string tmp = command; if (!extensions.empty()) tmp += " " + extensions; tmp += " " + quote(f1) + " " + quote(f2); SystemCmd cmd(tmp); for (const string& line : cmd.stdout()) cout << line << endl; for (const string& line : cmd.stderr()) cerr << line << endl; } snapper-0.5.4/client/misc.h000066400000000000000000000025371323360630300155600ustar00rootroot00000000000000/* * Copyright (c) [2011-2014] Novell, Inc. * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include using namespace snapper; using namespace std; unsigned int read_num(const string& str); map read_userdata(const string& s, const map& old = map()); string show_userdata(const map& userdata); map read_configdata(const list& l, const map& old = map()); string username(uid_t uid); struct Differ { Differ(); void run(const string& f1, const string& f2) const; string command; string extensions; }; snapper-0.5.4/client/mksubvolume.cc000066400000000000000000000236211323360630300173310ustar00rootroot00000000000000/* * Copyright (c) [2015-2017] SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include #include #include #include #include #include #include #include #include "snapper/BtrfsUtils.h" #include "snapper/AppUtil.h" #include "snapper/MntTable.h" #include "utils/GetOpts.h" using namespace snapper; using namespace BtrfsUtils; string target; bool set_nocow = false; bool verbose = false; void do_tmp_mount(const libmnt_fs* fs, const char* tmp_mountpoint, const string& subvol_option) { if (verbose) cout << "do-tmp-mount" << endl; libmnt_fs* x = mnt_copy_fs(NULL, fs); if (!x) throw runtime_error("mnt_copy_fs failed"); struct libmnt_context* cxt = mnt_new_context(); mnt_context_set_fs(cxt, x); mnt_context_set_target(cxt, tmp_mountpoint); if (!subvol_option.empty()) mnt_context_set_options(cxt, ("subvol=" + subvol_option).c_str()); else mnt_context_set_options(cxt, "subvolid=5"); // 5 is the btrfs top-level subvolume int ret = mnt_context_mount(cxt); if (ret != 0) throw runtime_error(sformat("mnt_context_mount failed, ret:%d", ret)); mnt_free_context(cxt); mnt_free_fs(x); } void do_create_subvolume(const string& tmp_mountpoint, const string& subvolume_name) { if (verbose) cout << "do-create-subvolume" << endl; string parent = tmp_mountpoint + "/" + dirname(subvolume_name); if (mkdir(parent.c_str(), 0777) != 0 && errno != EEXIST) throw runtime_error_with_errno("mkdir failed", errno); int fd = open(parent.c_str(), O_RDONLY); if (fd < 0) throw runtime_error_with_errno("open failed", errno); create_subvolume(fd, basename(subvolume_name)); close(fd); } void do_tmp_umount(const libmnt_fs* fs, const char* tmp_mountpoint) { if (verbose) cout << "do-tmp-umount" << endl; system("/sbin/udevadm settle --timeout 20"); libmnt_fs* x = mnt_copy_fs(NULL, fs); if (!x) throw runtime_error("mnt_copy_fs failed"); struct libmnt_context* cxt = mnt_new_context(); mnt_context_set_fs(cxt, x); mnt_context_set_target(cxt, tmp_mountpoint); int ret = mnt_context_umount(cxt); if (ret != 0) throw runtime_error(sformat("mnt_context_umount failed, ret:%d", ret)); mnt_free_context(cxt); mnt_free_fs(x); } void do_add_fstab_and_mount(MntTable& mnt_table, const libmnt_fs* fs, const string& subvol_option, const string& subvolume_name) { if (verbose) cout << "do-add-fstab-and-mount" << endl; libmnt_fs* x = mnt_copy_fs(NULL, fs); if (!x) throw runtime_error("mnt_copy_fs failed"); mnt_fs_set_target(x, target.c_str()); string full_subvol_option = subvolume_name; if (!subvol_option.empty()) full_subvol_option.insert(0, subvol_option + "/"); char* options = mnt_fs_strdup_options(x); mnt_optstr_remove_option(&options, "defaults"); mnt_optstr_remove_option(&options, "ro"); mnt_optstr_set_option(&options, "subvol", full_subvol_option.c_str()); mnt_fs_set_options(x, options); free(options); // Caution: mnt_context_mount may change the source of x so the fstab // functions must be called first. mnt_table.add_fs(x); mnt_table.replace_file(); if (mkdir(target.c_str(), 0777) != 0 && errno != EEXIST) throw runtime_error_with_errno("mkdir failed", errno); struct libmnt_context* cxt = mnt_new_context(); mnt_context_set_fs(cxt, x); int ret = mnt_context_mount(cxt); if (ret != 0) throw runtime_error(sformat("mnt_context_mount failed, ret:%d", ret)); mnt_free_context(cxt); mnt_free_fs(x); } void do_set_nocow() { if (!set_nocow) return; if (verbose) cout << "do-set-nocow" << endl; int fd = open(target.c_str(), O_RDONLY); if (fd == -1) { throw runtime_error_with_errno("open failed", errno); } unsigned long flags = 0; if (ioctl(fd, EXT2_IOC_GETFLAGS, &flags) == -1) { close(fd); throw runtime_error_with_errno("ioctl(EXT2_IOC_GETFLAGS) failed", errno); } flags |= FS_NOCOW_FL; if (ioctl(fd, EXT2_IOC_SETFLAGS, &flags) == -1) { close(fd); throw runtime_error_with_errno("ioctl(EXT2_IOC_SETFLAGS) failed", errno); } close(fd); } bool is_subvol_mount(const string& fs_options) { vector tmp1; boost::split(tmp1, fs_options, boost::is_any_of(","), boost::token_compress_on); for (const string& tmp2 : tmp1) { if (boost::starts_with(tmp2, "subvol=") || boost::starts_with(tmp2, "subvolid=")) return true; } return false; } libmnt_fs* find_filesystem(MntTable& mnt_table) { string tmp = target; for (;;) { libmnt_fs* fs = mnt_table.find_target_up(tmp, MNT_ITER_FORWARD); if (!fs) throw runtime_error("filesystem not found"); string fs_device = mnt_fs_get_source(fs); string fs_fstype = mnt_fs_get_fstype(fs); string fs_target = mnt_fs_get_target(fs); string fs_options = mnt_fs_get_options(fs); if (verbose) { cout << "fs-device:" << fs_device << endl; cout << "fs-fstype:" << fs_fstype << endl; cout << "fs-target:" << fs_target << endl; cout << "fs-options:" << fs_options << endl; } if (fs_fstype != "btrfs") throw runtime_error("filesystem is not btrfs"); if (fs_target == target) throw runtime_error("target exists in fstab"); if (!is_subvol_mount(fs_options)) return fs; if (verbose) cout << "ignoring subvol mount" << endl; if (tmp == "/") throw runtime_error("filesystem not found"); tmp = dirname(fs_target); } } /* * The used algorithm is as follow: * * 1. Search upwards from target for a filesystem (not mounted with subvol * option). The filesystem must of course be btrfs. * * 2. Determine the name for new subvolume: It is the target name without the * leading filesystem target (mountpoint). * * 3. Temporarily mount the filesystem and create new subvolume. Reasons for * the mount are documented in bsc #910602. * * 4. Create new subvolume. * * 5. Add new subvolume mount to fstab and mount new subvolume. */ void doit() { if (verbose) cout << "target:" << target << endl; if (target.empty() || boost::ends_with(target, "/")) throw runtime_error("invalid target"); if (access(target.c_str(), F_OK) == 0) throw runtime_error("target exists"); if (access(dirname(target).c_str(), F_OK) != 0) throw runtime_error("parent of target does not exist"); MntTable mnt_table("/"); mnt_table.parse_fstab(); // Find filesystem. libmnt_fs* fs = find_filesystem(mnt_table); string fs_target = mnt_fs_get_target(fs); // Get default subvolume of filesystem. int fd = open(fs_target.c_str(), O_RDONLY); if (fd == -1) throw runtime_error_with_errno("open failed", errno); subvolid_t default_subvolume_id = get_default_id(fd); if (verbose) cout << "default-subvolume-id:" << default_subvolume_id << endl; string default_subvolume_name = get_subvolume(fd, default_subvolume_id); if (verbose) cout << "default-subvolume-name:" << default_subvolume_name << endl; close(fd); // Determine subvol mount-option for tmp mount. The '@' is used on SLE. string subvol_option = ""; if (default_subvolume_name == "@" || boost::starts_with(default_subvolume_name, "@/")) subvol_option = "@"; if (verbose) cout << "subvol-option:" << subvol_option << endl; // Determine name for new subvolume: It is the target name without the // leading filesystem target. string subvolume_name = target.substr(fs_target.size() + (fs_target == "/" ? 0 : 1)); if (verbose) cout << "subvolume-name:" << subvolume_name << endl; // Execute all steps. char* tmp_mountpoint = strdup("/tmp/mksubvolume-XXXXXX"); if (!mkdtemp(tmp_mountpoint)) throw runtime_error_with_errno("mkdtemp failed", errno); do_tmp_mount(fs, tmp_mountpoint, subvol_option); try { do_create_subvolume(tmp_mountpoint, subvolume_name); } catch (...) { // Rethrow the original exception, not a potential exception of do_tmp_umount. try { do_tmp_umount(fs, tmp_mountpoint); rmdir(tmp_mountpoint); free(tmp_mountpoint); } catch (...) { } throw; } do_tmp_umount(fs, tmp_mountpoint); rmdir(tmp_mountpoint); free(tmp_mountpoint); do_add_fstab_and_mount(mnt_table, fs, subvol_option, subvolume_name); do_set_nocow(); } void usage() __attribute__ ((__noreturn__)); void usage() { cerr << "usage: [--nocow] [--verbose] target" << endl; exit(EXIT_FAILURE); } int main(int argc, char** argv) { setlocale(LC_ALL, ""); const struct option options[] = { { "nocow", no_argument, 0, 0 }, { "verbose", no_argument, 0, 'v' }, { 0, 0, 0, 0 } }; GetOpts getopts; getopts.init(argc, argv); GetOpts::parsed_opts opts = getopts.parse(options); GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("nocow")) != opts.end()) set_nocow = true; if ((opt = opts.find("verbose")) != opts.end()) verbose = true; if (getopts.numArgs() != 1) usage(); target = getopts.popArg(); try { doit(); exit(EXIT_SUCCESS); } catch (const std::exception& e) { cerr << "failure (" << e.what() << ")" << endl; exit(EXIT_FAILURE); } } snapper-0.5.4/client/proxy-dbus.cc000066400000000000000000000211361323360630300170730ustar00rootroot00000000000000/* * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include "proxy-dbus.h" #include "commands.h" #include "utils/text.h" using namespace std; ProxySnapshotDbus::ProxySnapshotDbus(ProxySnapshotsDbus* backref, unsigned int num) : backref(backref), num(num) { XSnapshot x = command_get_xsnapshot(conn(), configName(), num); type = x.type; date = x.date; uid = x.uid; pre_num = x.pre_num; description = x.description; cleanup = x.cleanup; userdata = x.userdata; } ProxySnapshotDbus::ProxySnapshotDbus(ProxySnapshotsDbus* backref, SnapshotType type, unsigned int num, time_t date, uid_t uid, unsigned int pre_num, const string& description, const string& cleanup, const map& userdata) : backref(backref), type(type), num(num), date(date), uid(uid), pre_num(pre_num), description(description), cleanup(cleanup), userdata(userdata) { } string ProxySnapshotDbus::mountFilesystemSnapshot(bool user_request) const { return command_mount_snapshot(conn(), configName(), num, user_request); } void ProxySnapshotDbus::umountFilesystemSnapshot(bool user_request) const { command_umount_snapshot(conn(), configName(), num, user_request); } DBus::Connection& ProxySnapshotDbus::conn() const { return backref->conn(); } const string& ProxySnapshotDbus::configName() const { return backref->configName(); } ProxySnapshotsDbus::ProxySnapshotsDbus(ProxySnapperDbus* backref) : backref(backref) { XSnapshots tmp = command_list_xsnapshots(conn(), configName()); for (XSnapshots::const_iterator it = tmp.begin(); it != tmp.end(); ++it) proxy_snapshots.push_back(new ProxySnapshotDbus(this, it->getType(), it->getNum(), it->getDate(), it->getUid(), it->getPreNum(), it->getDescription(), it->getCleanup(), it->getUserdata())); } DBus::Connection& ProxySnapshotsDbus::conn() const { return backref->conn(); } const string& ProxySnapshotsDbus::configName() const { return backref->config_name; } ProxyConfig ProxySnapperDbus::getConfig() const { XConfigInfo tmp = command_get_xconfig(conn(), config_name); return ProxyConfig(tmp.raw); } void ProxySnapperDbus::setConfig(const ProxyConfig& proxy_config) { command_set_xconfig(conn(), config_name, proxy_config.getAllValues()); } void ProxySnapperDbus::setupQuota() { command_setup_quota(conn(), config_name); } void ProxySnapperDbus::prepareQuota() const { command_prepare_quota(conn(), config_name); } QuotaData ProxySnapperDbus::queryQuotaData() const { return command_query_quota(conn(), config_name); } ProxySnapshots::const_iterator ProxySnapperDbus::createSingleSnapshot(const SCD& scd) { unsigned int num = command_create_single_snapshot(conn(), config_name, scd.description, scd.cleanup, scd.userdata); proxy_snapshots.emplace_back(new ProxySnapshotDbus(&proxy_snapshots, num)); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperDbus::createSingleSnapshot(ProxySnapshots::const_iterator parent, const SCD& scd) { unsigned int num = command_create_single_snapshot_v2(conn(), config_name, parent->getNum(), scd.read_only, scd.description, scd.cleanup, scd.userdata); proxy_snapshots.emplace_back(new ProxySnapshotDbus(&proxy_snapshots, num)); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperDbus::createSingleSnapshotOfDefault(const SCD& scd) { unsigned int num = command_create_single_snapshot_of_default(conn(), config_name, scd.read_only, scd.description, scd.cleanup, scd.userdata); proxy_snapshots.emplace_back(new ProxySnapshotDbus(&proxy_snapshots, num)); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperDbus::createPreSnapshot(const SCD& scd) { unsigned int num = command_create_pre_snapshot(conn(), config_name, scd.description, scd.cleanup, scd.userdata); proxy_snapshots.emplace_back(new ProxySnapshotDbus(&proxy_snapshots, num)); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperDbus::createPostSnapshot(ProxySnapshots::const_iterator pre, const SCD& scd) { unsigned int num = command_create_post_snapshot(conn(), config_name, pre->getNum(), scd.description, scd.cleanup, scd.userdata); proxy_snapshots.emplace_back(new ProxySnapshotDbus(&proxy_snapshots, num)); return --proxy_snapshots.end(); } void ProxySnapperDbus::modifySnapshot(ProxySnapshots::iterator snapshot, const SMD& smd) { command_set_snapshot(conn(), config_name, snapshot->getNum(), smd); } void ProxySnapperDbus::deleteSnapshots(vector snapshots, bool verbose) { vector nums; for (const ProxySnapshots::iterator& proxy_snapshot : snapshots) nums.push_back(proxy_snapshot->getNum()); command_delete_snapshots(conn(), config_name, nums, verbose); ProxySnapshots& proxy_snapshots = getSnapshots(); for (ProxySnapshots::iterator& proxy_snapshot : snapshots) proxy_snapshots.erase(proxy_snapshot); } ProxyComparison ProxySnapperDbus::createComparison(const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) { return ProxyComparison(new ProxyComparisonDbus(this, lhs, rhs, mount)); } void ProxySnapperDbus::syncFilesystem() const { command_sync(conn(), config_name); } DBus::Connection& ProxySnapperDbus::conn() const { return backref->conn; } void ProxySnappersDbus::createConfig(const string& config_name, const string& subvolume, const string& fstype, const string& template_name) { command_create_config(conn, config_name, subvolume, fstype, template_name); } void ProxySnappersDbus::deleteConfig(const string& config_name) { command_delete_config(conn, config_name); } ProxySnapper* ProxySnappersDbus::getSnapper(const string& config_name) { for (unique_ptr& proxy_snapper : proxy_snappers) { if (proxy_snapper->config_name == config_name) return proxy_snapper.get(); } ProxySnapperDbus* ret = new ProxySnapperDbus(this, config_name); proxy_snappers.emplace_back(ret); return ret; } map ProxySnappersDbus::getConfigs() const { map ret; vector config_infos = command_list_xconfigs(conn); for (XConfigInfo& x : config_infos) ret.emplace(make_pair(x.config_name, x.raw)); return ret; } vector ProxySnappersDbus::debug() const { return command_debug(conn); } ProxyComparisonDbus::ProxyComparisonDbus(ProxySnapperDbus* backref, const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) : backref(backref), lhs(lhs), rhs(rhs), files(&file_paths) { command_create_comparison(conn(), configName(), lhs.getNum(), rhs.getNum()); file_paths.system_path = command_get_mount_point(backref->conn(), backref->config_name, 0); if (mount) { if (!lhs.isCurrent()) file_paths.pre_path = command_mount_snapshot(backref->conn(), backref->config_name, lhs.getNum(), false); else file_paths.pre_path = file_paths.system_path; if (!rhs.isCurrent()) file_paths.post_path = command_mount_snapshot(backref->conn(), backref->config_name, rhs.getNum(), false); else file_paths.post_path = file_paths.system_path; } vector tmp1 = command_get_xfiles(backref->conn(), backref->config_name, lhs.getNum(), rhs.getNum()); vector tmp2; for (const XFile& xfile : tmp1) tmp2.emplace_back(&file_paths, xfile.name, xfile.status); files = Files(&file_paths, tmp2); } ProxyComparisonDbus::~ProxyComparisonDbus() { command_delete_comparison(conn(), configName(), lhs.getNum(), rhs.getNum()); } DBus::Connection& ProxyComparisonDbus::conn() const { return backref->conn(); } const string& ProxyComparisonDbus::configName() const { return backref->config_name; } ProxySnappers ProxySnappers::createDbus() { return ProxySnappers(new ProxySnappersDbus()); } snapper-0.5.4/client/proxy-dbus.h000066400000000000000000000133271323360630300167400ustar00rootroot00000000000000/* * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #ifndef SNAPPER_PROXY_DBUS_H #define SNAPPER_PROXY_DBUS_H #include "dbus/DBusMessage.h" #include "dbus/DBusConnection.h" #include "proxy.h" class ProxySnapshotDbus; class ProxySnapshotsDbus; class ProxySnapperDbus; class ProxySnappersDbus; /** * Concrete class of ProxySnapshot for DBus communication. Store all snapshot * data in the client to avoid numerous DBus queries. */ class ProxySnapshotDbus : public ProxySnapshot::Impl { public: ProxySnapshotDbus(ProxySnapshotsDbus* backref, SnapshotType type, unsigned int num, time_t date, uid_t uid, unsigned int pre_num, const string& description, const string& cleanup, const map& userdata); ProxySnapshotDbus(ProxySnapshotsDbus* backref, unsigned int num); virtual SnapshotType getType() const override { return type; } virtual unsigned int getNum() const override { return num; } virtual time_t getDate() const override { return date; } virtual uid_t getUid() const override { return uid; } virtual unsigned int getPreNum() const override { return pre_num; } virtual const string& getDescription() const override { return description; } virtual const string& getCleanup() const override { return cleanup; } virtual const map& getUserdata() const override { return userdata; } virtual bool isCurrent() const override { return num == 0; } virtual string mountFilesystemSnapshot(bool user_request) const override; virtual void umountFilesystemSnapshot(bool user_request) const override; DBus::Connection& conn() const; const string& configName() const; private: ProxySnapshotsDbus* backref; SnapshotType type; unsigned int num; time_t date; uid_t uid; unsigned int pre_num; string description; string cleanup; map userdata; }; class ProxySnapshotsDbus : public ProxySnapshots { public: ProxySnapshotsDbus(ProxySnapperDbus* backref); DBus::Connection& conn() const; const string& configName() const; private: ProxySnapperDbus* backref; }; class ProxySnapperDbus : public ProxySnapper { public: ProxySnapperDbus(ProxySnappersDbus* backref, const string& config_name) : backref(backref), config_name(config_name), proxy_snapshots(this) {} virtual const string& configName() const override { return config_name; } virtual ProxyConfig getConfig() const override; virtual void setConfig(const ProxyConfig& proxy_config) override; virtual ProxySnapshots::const_iterator createSingleSnapshot(const SCD& scd) override; virtual ProxySnapshots::const_iterator createSingleSnapshot(ProxySnapshots::const_iterator parent, const SCD& scd) override; virtual ProxySnapshots::const_iterator createSingleSnapshotOfDefault(const SCD& scd) override; virtual ProxySnapshots::const_iterator createPreSnapshot(const SCD& scd) override; virtual ProxySnapshots::const_iterator createPostSnapshot(ProxySnapshots::const_iterator pre, const SCD& scd) override; virtual void modifySnapshot(ProxySnapshots::iterator snapshot, const SMD& smd) override; virtual void deleteSnapshots(vector snapshots, bool verbose) override; virtual ProxyComparison createComparison(const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) override; virtual void syncFilesystem() const override; virtual ProxySnapshots& getSnapshots() override { return proxy_snapshots; } virtual const ProxySnapshots& getSnapshots() const override { return proxy_snapshots; } virtual void setupQuota() override; virtual void prepareQuota() const override; virtual QuotaData queryQuotaData() const override; DBus::Connection& conn() const; private: ProxySnappersDbus* backref; public: string config_name; ProxySnapshotsDbus proxy_snapshots; }; class ProxySnappersDbus : public ProxySnappers::Impl { public: ProxySnappersDbus() : conn(DBUS_BUS_SYSTEM) {} virtual void createConfig(const string& config_name, const string& subvolume, const string& fstype, const string& template_name) override; virtual void deleteConfig(const string& config_name) override; virtual ProxySnapper* getSnapper(const string& config_name) override; virtual map getConfigs() const override; virtual vector debug() const override; mutable DBus::Connection conn; list> proxy_snappers; }; class ProxyComparisonDbus : public ProxyComparison::Impl { public: ProxyComparisonDbus(ProxySnapperDbus* backref, const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount); ~ProxyComparisonDbus(); virtual const Files& getFiles() const override { return files; } DBus::Connection& conn() const; const string& configName() const; private: ProxySnapperDbus* backref; const ProxySnapshot& lhs; const ProxySnapshot& rhs; FilePaths file_paths; Files files; }; #endif snapper-0.5.4/client/proxy-lib.cc000066400000000000000000000115001323360630300166760ustar00rootroot00000000000000/* * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include "proxy-lib.h" using namespace std; ProxyConfig ProxySnapperLib::getConfig() const { return ProxyConfig(snapper->getConfigInfo().getAllValues()); } void ProxySnapperLib::setConfig(const ProxyConfig& proxy_config) { snapper->setConfigInfo(proxy_config.getAllValues()); } ProxySnapshots::const_iterator ProxySnapperLib::createSingleSnapshot(const SCD& scd) { proxy_snapshots.emplace_back(new ProxySnapshotLib(snapper->createSingleSnapshot(scd))); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperLib::createSingleSnapshot(ProxySnapshots::const_iterator parent, const SCD& scd) { proxy_snapshots.emplace_back(new ProxySnapshotLib(snapper->createSingleSnapshot(to_lib(*parent).it, scd))); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperLib::createSingleSnapshotOfDefault(const SCD& scd) { proxy_snapshots.emplace_back(new ProxySnapshotLib(snapper->createSingleSnapshotOfDefault(scd))); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperLib::createPreSnapshot(const SCD& scd) { proxy_snapshots.emplace_back(new ProxySnapshotLib(snapper->createPreSnapshot(scd))); return --proxy_snapshots.end(); } ProxySnapshots::const_iterator ProxySnapperLib::createPostSnapshot(ProxySnapshots::const_iterator pre, const SCD& scd) { proxy_snapshots.emplace_back(new ProxySnapshotLib(snapper->createPostSnapshot(to_lib(*pre).it, scd))); return --proxy_snapshots.end(); } void ProxySnapperLib::modifySnapshot(ProxySnapshots::iterator snapshot, const SMD& smd) { snapper->modifySnapshot(to_lib(*snapshot).it, smd); } void ProxySnapperLib::deleteSnapshots(vector snapshots, bool verbose) { for (ProxySnapshots::iterator& snapshot : snapshots) snapper->deleteSnapshot(to_lib(*snapshot).it); ProxySnapshots& proxy_snapshots = getSnapshots(); for (ProxySnapshots::iterator& proxy_snapshot : snapshots) proxy_snapshots.erase(proxy_snapshot); } ProxyComparison ProxySnapperLib::createComparison(const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) { return ProxyComparison(new ProxyComparisonLib(this, lhs, rhs, mount)); } ProxySnapshotsLib::ProxySnapshotsLib(ProxySnapperLib* backref) : backref(backref) { Snapshots& tmp = backref->snapper->getSnapshots(); for (Snapshots::iterator it = tmp.begin(); it != tmp.end(); ++it) proxy_snapshots.push_back(new ProxySnapshotLib(it)); } void ProxySnappersLib::createConfig(const string& config_name, const string& subvolume, const string& fstype, const string& template_name) { Snapper::createConfig(config_name, target_root, subvolume, fstype, template_name); } void ProxySnappersLib::deleteConfig(const string& config_name) { Snapper::deleteConfig(config_name, target_root); } ProxySnapper* ProxySnappersLib::getSnapper(const string& config_name) { for (unique_ptr& proxy_snapper : proxy_snappers) { if (proxy_snapper->snapper->configName() == config_name) return proxy_snapper.get(); } ProxySnapperLib* ret = new ProxySnapperLib(config_name, target_root); proxy_snappers.emplace_back(ret); return ret; } map ProxySnappersLib::getConfigs() const { map ret; list config_infos = Snapper::getConfigs(target_root); for (const ConfigInfo& config_info : config_infos) ret.emplace(make_pair(config_info.getConfigName(), config_info.getAllValues())); return ret; } ProxyComparisonLib::ProxyComparisonLib(ProxySnapperLib* proxy_snapper, const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) : proxy_snapper(proxy_snapper) { comparison.reset(new Comparison(proxy_snapper->snapper.get(), to_lib(lhs).it, to_lib(rhs).it, mount)); } ProxySnappers ProxySnappers::createLib(const string& target_root) { return ProxySnappers(new ProxySnappersLib(target_root)); } const ProxySnapshotLib& to_lib(const ProxySnapshot& proxy_snapshot) { return dynamic_cast(proxy_snapshot.get_impl()); } snapper-0.5.4/client/proxy-lib.h000066400000000000000000000122441323360630300165460ustar00rootroot00000000000000/* * Copyright (c) 2016 SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #ifndef SNAPPER_PROXY_LIB_H #define SNAPPER_PROXY_LIB_H #include "proxy.h" #include #include class ProxySnapshotLib : public ProxySnapshot::Impl { public: ProxySnapshotLib(Snapshots::iterator it) : it(it) {} virtual SnapshotType getType() const override { return it->getType(); } virtual unsigned int getNum() const override { return it->getNum(); } virtual time_t getDate() const override { return it->getDate(); } virtual uid_t getUid() const override { return it->getUid(); } virtual unsigned int getPreNum() const override { return it->getPreNum(); } virtual const string& getDescription() const override { return it->getDescription(); } virtual const string& getCleanup() const override { return it->getCleanup(); } virtual const map& getUserdata() const override { return it->getUserdata(); } virtual bool isCurrent() const override { return it->isCurrent(); } virtual string mountFilesystemSnapshot(bool user_request) const override { it->mountFilesystemSnapshot(user_request); return it->snapshotDir(); } virtual void umountFilesystemSnapshot(bool user_request) const override { it->umountFilesystemSnapshot(user_request); } Snapshots::iterator it; }; class ProxySnapperLib; class ProxySnapshotsLib : public ProxySnapshots { public: ProxySnapshotsLib(ProxySnapperLib* backref); ProxySnapperLib* backref; }; class ProxySnapperLib : public ProxySnapper { public: ProxySnapperLib(const string& config_name, const string& target_root) : snapper(new Snapper(config_name, target_root)), proxy_snapshots(this) {} virtual const string& configName() const override { return snapper->configName(); } virtual ProxyConfig getConfig() const override; virtual void setConfig(const ProxyConfig& proxy_config) override; virtual ProxySnapshots::const_iterator createSingleSnapshot(const SCD& scd) override; virtual ProxySnapshots::const_iterator createSingleSnapshot(ProxySnapshots::const_iterator parent, const SCD& scd) override; virtual ProxySnapshots::const_iterator createSingleSnapshotOfDefault(const SCD& scd) override; virtual ProxySnapshots::const_iterator createPreSnapshot(const SCD& scd) override; virtual ProxySnapshots::const_iterator createPostSnapshot(ProxySnapshots::const_iterator pre, const SCD& scd) override; virtual void modifySnapshot(ProxySnapshots::iterator snapshot, const SMD& smd) override; virtual void deleteSnapshots(vector snapshots, bool verbose) override; virtual ProxyComparison createComparison(const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) override; virtual void syncFilesystem() const override { snapper->syncFilesystem(); } virtual ProxySnapshots& getSnapshots() override { return proxy_snapshots; } virtual const ProxySnapshots& getSnapshots() const override { return proxy_snapshots; } virtual void setupQuota() override { snapper->setupQuota(); } virtual void prepareQuota() const override { snapper->prepareQuota(); } virtual QuotaData queryQuotaData() const override { return snapper->queryQuotaData(); } std::unique_ptr snapper; private: ProxySnapshotsLib proxy_snapshots; }; class ProxySnappersLib : public ProxySnappers::Impl { public: ProxySnappersLib(const string& target_root) : target_root(target_root) {} virtual void createConfig(const string& config_name, const string& subvolume, const string& fstype, const string& template_name) override; virtual void deleteConfig(const string& config_name) override; virtual ProxySnapper* getSnapper(const string& config_name) override; virtual map getConfigs() const override; virtual vector debug() const { return Snapper::debug(); } private: const string target_root; list> proxy_snappers; }; class ProxyComparisonLib : public ProxyComparison::Impl { public: ProxyComparisonLib(ProxySnapperLib* proxy_snapper, const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount); virtual const Files& getFiles() const override { return comparison->getFiles(); } ProxySnapper* proxy_snapper; private: std::unique_ptr comparison; }; const ProxySnapshotLib& to_lib(const ProxySnapshot& proxy_snapshot); #endif snapper-0.5.4/client/proxy.cc000066400000000000000000000104041323360630300161340ustar00rootroot00000000000000/* * Copyright (c) [2016-2017] SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include #include #include #include #include #include "utils/text.h" #include "proxy.h" using namespace std; string ProxyConfig::getSubvolume() const { string subvolume; getValue(KEY_SUBVOLUME, subvolume); return subvolume; } bool ProxyConfig::getValue(const string& key, string& value) const { map::const_iterator it = values.find(key); if (it == values.end()) return false; value = it->second; return true; } SMD ProxySnapshot::getSmd() const { SMD smd; smd.description = getDescription(); smd.cleanup = getCleanup(); smd.userdata = getUserdata(); return smd; } ProxySnapshots::iterator ProxySnapshots::find(unsigned int num) { return find_if(proxy_snapshots.begin(), proxy_snapshots.end(), [num](const ProxySnapshot& x) { return x.getNum() == num; }); } ProxySnapshots::const_iterator ProxySnapshots::find(unsigned int num) const { return find_if(proxy_snapshots.begin(), proxy_snapshots.end(), [num](const ProxySnapshot& x) { return x.getNum() == num; }); } ProxySnapshots::iterator ProxySnapshots::findNum(const string& str) { istringstream s(str); unsigned int num = 0; s >> num; if (s.fail() || !s.eof()) { cerr << sformat(_("Invalid snapshot '%s'."), str.c_str()) << endl; exit(EXIT_FAILURE); } iterator ret = find(num); if (ret == proxy_snapshots.end()) { cerr << sformat(_("Snapshot '%u' not found."), num) << endl; exit(EXIT_FAILURE); } return ret; } ProxySnapshots::const_iterator ProxySnapshots::findNum(const string& str) const { istringstream s(str); unsigned int num = 0; s >> num; if (s.fail() || !s.eof()) { cerr << sformat(_("Invalid snapshot '%s'."), str.c_str()) << endl; exit(EXIT_FAILURE); } const_iterator ret = find(num); if (ret == proxy_snapshots.end()) { cerr << sformat(_("Snapshot '%u' not found."), num) << endl; exit(EXIT_FAILURE); } return ret; } pair ProxySnapshots::findNums(const string& str, const string& delim) { string::size_type pos = str.find(delim); if (pos == string::npos) { if (delim == "..") { cerr << _("Missing delimiter '..' between snapshot numbers.") << endl << _("See 'man snapper' for further instructions.") << endl; exit(EXIT_FAILURE); } cerr << _("Invalid snapshots.") << endl; exit(EXIT_FAILURE); } ProxySnapshots::iterator num1 = findNum(str.substr(0, pos)); ProxySnapshots::iterator num2 = findNum(str.substr(pos + delim.size())); if (num1->getNum() == num2->getNum()) { cerr << _("Identical snapshots.") << endl; exit(EXIT_FAILURE); } return make_pair(num1, num2); } ProxySnapshots::const_iterator ProxySnapshots::findPre(const_iterator post) const { for (const_iterator it = begin(); it != end(); ++it) { if (it->getType() == PRE && it->getNum() == post->getPreNum()) return it; } return end(); } ProxySnapshots::iterator ProxySnapshots::findPost(iterator pre) { for (iterator it = begin(); it != end(); ++it) { if (it->getType() == POST && it->getPreNum() == pre->getNum()) return it; } return end(); } ProxySnapshots::const_iterator ProxySnapshots::findPost(const_iterator pre) const { for (const_iterator it = begin(); it != end(); ++it) { if (it->getType() == POST && it->getPreNum() == pre->getNum()) return it; } return end(); } snapper-0.5.4/client/proxy.h000066400000000000000000000205411323360630300160010ustar00rootroot00000000000000/* * Copyright (c) [2016-2017] SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #ifndef SNAPPER_PROXY_H #define SNAPPER_PROXY_H #include #include #include #include #include #include #include using namespace snapper; /** * The proxy classes here allow clients, so far only the snapper command line * interface, to work with and without DBus in a transparent way by providing * the same interface in both cases. * * The main idea for providing the same interface is to have an abstract * class, e.g. ProxySnapper, and two concrete implementation ProxySnapperDbus * and ProxySnapperLib. This is done for ProxySnapshots, ProxySnapper and * ProxySnappers. * * For ProxySnapshot the implementation is more complicated since * ProxySnapshots provides an interface to list and thus * polymorphism does not work. Instead ProxySnapshot has an implementation * pointer (pimpl idiom) which ensures polymorphism. Another possibility would * be to provide an interface to list in ProxySnapshots but * that is less intuitive to use. * * All objects are stored in containers or smart pointers to avoid manual * cleanup. * * The interface copycats the classes Snapshot, Snapshots and Snapper of * libsnapper. For consistency one may add a Snappers class to libsnapper. * * Limitations: The classes are tailored for the snapper command line * interface. Other use-cases are not supported. */ // TODO maybe unique error handling, e.g. catch dbus exceptions and throw // snapper or new exceptions class ProxyConfig { public: ProxyConfig(const map& values) : values(values) {} const map& getAllValues() const { return values; } string getSubvolume() const; bool getValue(const string& key, string& value) const; private: map values; }; class ProxySnapshot { public: SnapshotType getType() const { return impl->getType(); } unsigned int getNum() const { return impl->getNum(); } time_t getDate() const { return impl->getDate(); } uid_t getUid() const { return impl->getUid(); } unsigned int getPreNum() const { return impl->getPreNum(); } const string& getDescription() const { return impl->getDescription(); } const string& getCleanup() const { return impl->getCleanup(); } const map& getUserdata() const { return impl->getUserdata(); } SMD getSmd() const; bool isCurrent() const { return impl->isCurrent(); } void mountFilesystemSnapshot(bool user_request) const { impl->mountFilesystemSnapshot(user_request); } void umountFilesystemSnapshot(bool user_request) const { impl->umountFilesystemSnapshot(user_request); } public: class Impl { public: virtual ~Impl() {} virtual SnapshotType getType() const = 0; virtual unsigned int getNum() const = 0; virtual time_t getDate() const = 0; virtual uid_t getUid() const = 0; virtual unsigned int getPreNum() const = 0; virtual const string& getDescription() const = 0; virtual const string& getCleanup() const = 0; virtual const map& getUserdata() const = 0; virtual bool isCurrent() const = 0; virtual string mountFilesystemSnapshot(bool user_request) const = 0; virtual void umountFilesystemSnapshot(bool user_request) const = 0; }; ProxySnapshot(Impl* impl) : impl(impl) {} Impl& get_impl() { return *impl; } const Impl& get_impl() const { return *impl; } private: std::unique_ptr impl; }; class ProxySnapshots { public: virtual ~ProxySnapshots() {} typedef list::iterator iterator; typedef list::const_iterator const_iterator; iterator begin() { return proxy_snapshots.begin(); } const_iterator begin() const { return proxy_snapshots.begin(); } iterator end() { return proxy_snapshots.end(); } const_iterator end() const { return proxy_snapshots.end(); } const_iterator getCurrent() const { return proxy_snapshots.begin(); } iterator find(unsigned int i); const_iterator find(unsigned int i) const; iterator findNum(const string& str); const_iterator findNum(const string& str) const; std::pair findNums(const string& str, const string& delim = ".."); const_iterator findPre(const_iterator post) const; iterator findPost(iterator pre); const_iterator findPost(const_iterator pre) const; void emplace_back(ProxySnapshot::Impl* value) { proxy_snapshots.emplace_back(value); } void erase(iterator it) { proxy_snapshots.erase(it); } protected: list proxy_snapshots; }; class ProxyComparison; class ProxySnapper { public: virtual ~ProxySnapper() {} virtual const string& configName() const = 0; virtual ProxyConfig getConfig() const = 0; virtual void setConfig(const ProxyConfig& proxy_config) = 0; virtual ProxySnapshots::const_iterator createSingleSnapshot(const SCD& scd) = 0; virtual ProxySnapshots::const_iterator createSingleSnapshot(ProxySnapshots::const_iterator parent, const SCD& scd) = 0; virtual ProxySnapshots::const_iterator createSingleSnapshotOfDefault(const SCD& scd) = 0; virtual ProxySnapshots::const_iterator createPreSnapshot(const SCD& scd) = 0; virtual ProxySnapshots::const_iterator createPostSnapshot(ProxySnapshots::const_iterator pre, const SCD& scd) = 0; virtual void modifySnapshot(ProxySnapshots::iterator snapshot, const SMD& smd) = 0; virtual void deleteSnapshots(vector snapshots, bool verbose) = 0; virtual ProxyComparison createComparison(const ProxySnapshot& lhs, const ProxySnapshot& rhs, bool mount) = 0; virtual void syncFilesystem() const = 0; virtual ProxySnapshots& getSnapshots() = 0; virtual const ProxySnapshots& getSnapshots() const = 0; virtual void setupQuota() = 0; virtual void prepareQuota() const = 0; virtual QuotaData queryQuotaData() const = 0; }; class ProxySnappers { public: static ProxySnappers createDbus(); static ProxySnappers createLib(const string& target_root); void createConfig(const string& config_name, const string& subvolume, const string& fstype, const string& template_name) { return impl->createConfig(config_name, subvolume, fstype, template_name); } void deleteConfig(const string& config_name) { return impl->deleteConfig(config_name); } ProxySnapper* getSnapper(const string& config_name) { return impl->getSnapper(config_name); } map getConfigs() const { return impl->getConfigs(); } vector debug() const { return impl->debug(); } public: class Impl { public: virtual ~Impl() {} virtual void createConfig(const string& config_name, const string& subvolume, const string& fstype, const string& template_name) = 0; virtual void deleteConfig(const string& config_name) = 0; virtual ProxySnapper* getSnapper(const string& config_name) = 0; virtual map getConfigs() const = 0; virtual vector debug() const = 0; }; ProxySnappers(Impl* impl) : impl(impl) {} Impl& get_impl() { return *impl; } const Impl& get_impl() const { return *impl; } private: std::unique_ptr impl; }; class ProxyComparison { public: const Files& getFiles() const { return impl->getFiles(); } public: class Impl { public: virtual ~Impl() {} virtual const Files& getFiles() const = 0; }; ProxyComparison(Impl* impl) : impl(impl) {} Impl& get_impl() { return *impl; } const Impl& get_impl() const { return *impl; } private: std::unique_ptr impl; }; #endif snapper-0.5.4/client/snapper.cc000066400000000000000000001257011323360630300164320ustar00rootroot00000000000000/* * Copyright (c) [2011-2015] Novell, Inc. * Copyright (c) [2016-2017] SUSE LLC * * All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * under the terms of version 2 of the GNU General Public License as published * by the Free Software Foundation. * * 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, contact Novell, Inc. * * To contact Novell about this file by physical or electronic mail, you may * find current contact information at www.novell.com. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_ROLLBACK #include #include #endif #include "utils/text.h" #include "utils/Table.h" #include "utils/GetOpts.h" #include "cleanup.h" #include "errors.h" #include "proxy.h" #include "misc.h" using namespace snapper; using namespace std; struct Cmd { typedef void (*cmd_func_t)(ProxySnappers* snappers, ProxySnapper* snapper); typedef void (*help_func_t)(); Cmd(const string& name, cmd_func_t cmd_func, help_func_t help_func, bool needs_snapper) : name(name), aliases(), cmd_func(cmd_func), help_func(help_func), needs_snapper(needs_snapper) {} Cmd(const string& name, const vector& aliases, cmd_func_t cmd_func, help_func_t help_func, bool needs_snapper) : name(name), aliases(aliases), cmd_func(cmd_func), help_func(help_func), needs_snapper(needs_snapper) {} const string name; const vector aliases; const cmd_func_t cmd_func; const help_func_t help_func; const bool needs_snapper; }; GetOpts getopts; bool quiet = false; bool verbose = false; bool utc = false; bool iso = false; string config_name = "root"; bool no_dbus = false; string target_root = "/"; struct MyFiles : public Files { MyFiles(const Files& files) : Files(files) {} void bulk_process(FILE* file, std::function callback); }; void MyFiles::bulk_process(FILE* file, std::function callback) { if (file) { AsciiFileReader asciifile(file); string line; while (asciifile.getline(line)) { if (line.empty()) continue; string name = line; // strip optional status if (name[0] != '/') { string::size_type pos = name.find(" "); if (pos == string::npos) continue; name.erase(0, pos + 1); } Files::iterator it = findAbsolutePath(name); if (it == end()) { cerr << sformat(_("File '%s' not found."), name.c_str()) << endl; exit(EXIT_FAILURE); } callback(*it); } } else { if (getopts.numArgs() == 0) { for (Files::iterator it = begin(); it != end(); ++it) callback(*it); } else { while (getopts.numArgs() > 0) { string name = getopts.popArg(); Files::iterator it = findAbsolutePath(name); if (it == end()) { cerr << sformat(_("File '%s' not found."), name.c_str()) << endl; exit(EXIT_FAILURE); } callback(*it); } } } } void help_list_configs() { cout << _(" List configs:") << endl << _("\tsnapper list-configs") << endl << endl; } void command_list_configs(ProxySnappers* snappers, ProxySnapper*) { getopts.parse("list-configs", GetOpts::no_options); if (getopts.hasArgs()) { cerr << _("Command 'list-configs' does not take arguments.") << endl; exit(EXIT_FAILURE); } Table table; TableHeader header; header.add(_("Config")); header.add(_("Subvolume")); table.setHeader(header); map configs = snappers->getConfigs(); for (const map::value_type value : configs) { TableRow row; row.add(value.first); row.add(value.second.getSubvolume()); table.add(row); } cout << table; } void help_create_config() { cout << _(" Create config:") << endl << _("\tsnapper create-config ") << endl << endl << _(" Options for 'create-config' command:") << endl << _("\t--fstype, -f \t\tManually set filesystem type.") << endl << _("\t--template, -t \t\tName of config template to use.") << endl << endl; } void command_create_config(ProxySnappers* snappers, ProxySnapper*) { const struct option options[] = { { "fstype", required_argument, 0, 'f' }, { "template", required_argument, 0, 't' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("create-config", options); if (getopts.numArgs() != 1) { cerr << _("Command 'create-config' needs one argument.") << endl; exit(EXIT_FAILURE); } string subvolume = realpath(getopts.popArg()); if (subvolume.empty()) { cerr << _("Invalid subvolume.") << endl; exit(EXIT_FAILURE); } string fstype = ""; string template_name = "default"; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("fstype")) != opts.end()) fstype = opt->second; if ((opt = opts.find("template")) != opts.end()) template_name = opt->second; if (fstype.empty() && !Snapper::detectFstype(subvolume, fstype)) { cerr << _("Detecting filesystem type failed.") << endl; exit(EXIT_FAILURE); } snappers->createConfig(config_name, subvolume, fstype, template_name); } void help_delete_config() { cout << _(" Delete config:") << endl << _("\tsnapper delete-config") << endl << endl; } void command_delete_config(ProxySnappers* snappers, ProxySnapper*) { getopts.parse("delete-config", GetOpts::no_options); if (getopts.hasArgs()) { cerr << _("Command 'delete-config' does not take arguments.") << endl; exit(EXIT_FAILURE); } snappers->deleteConfig(config_name); } void help_get_config() { cout << _(" Get config:") << endl << _("\tsnapper get-config") << endl << endl; } void command_get_config(ProxySnappers* snappers, ProxySnapper* snapper) { getopts.parse("get-config", GetOpts::no_options); if (getopts.hasArgs()) { cerr << _("Command 'get-config' does not take arguments.") << endl; exit(EXIT_FAILURE); } Table table; TableHeader header; header.add(_("Key")); header.add(_("Value")); table.setHeader(header); ProxyConfig config = snapper->getConfig(); for (const map::value_type& value : config.getAllValues()) { TableRow row; row.add(value.first); row.add(value.second); table.add(row); } cout << table; } void help_set_config() { cout << _(" Set config:") << endl << _("\tsnapper set-config ") << endl << endl; } void command_set_config(ProxySnappers* snappers, ProxySnapper* snapper) { getopts.parse("set-config", GetOpts::no_options); if (!getopts.hasArgs()) { cerr << _("Command 'set-config' needs at least one argument.") << endl; exit(EXIT_FAILURE); } ProxyConfig config(read_configdata(getopts.getArgs())); snapper->setConfig(config); } void help_list() { cout << _(" List snapshots:") << endl << _("\tsnapper list") << endl << endl << _(" Options for 'list' command:") << endl << _("\t--type, -t \t\tType of snapshots to list.") << endl << _("\t--all-configs, -a\t\tList snapshots from all accessible configs.") << endl << endl; } enum ListMode { LM_ALL, LM_SINGLE, LM_PRE_POST }; void list_from_one_config(ProxySnapper* snapper, ListMode list_mode); void command_list(ProxySnappers* snappers, ProxySnapper*) { const struct option options[] = { { "type", required_argument, 0, 't' }, { "all-configs", no_argument, 0, 'a' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("list", options); if (getopts.hasArgs()) { cerr << _("Command 'list' does not take arguments.") << endl; exit(EXIT_FAILURE); } ListMode list_mode = LM_ALL; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("type")) != opts.end()) { if (opt->second == "all") list_mode = LM_ALL; else if (opt->second == "single") list_mode = LM_SINGLE; else if (opt->second == "pre-post") list_mode = LM_PRE_POST; else { cerr << _("Unknown type of snapshots.") << endl; exit(EXIT_FAILURE); } } vector tmp; if ((opt = opts.find("all-configs")) == opts.end()) { tmp.push_back(config_name); } else { map configs = snappers->getConfigs(); for (map::value_type it : configs) tmp.push_back(it.first); } for (vector::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { ProxySnapper* snapper = snappers->getSnapper(*it); if (it != tmp.begin()) cout << endl; if (tmp.size() > 1) { cout << "Config: " << snapper->configName() << ", subvolume: " << snapper->getConfig().getSubvolume() << endl; } list_from_one_config(snapper, list_mode); } } void list_from_one_config(ProxySnapper* snapper, ListMode list_mode) { Table table; switch (list_mode) { case LM_ALL: { TableHeader header; header.add(_("Type")); header.add(_("#")); header.add(_("Pre #")); header.add(_("Date")); header.add(_("User")); header.add(_("Cleanup")); header.add(_("Description")); header.add(_("Userdata")); table.setHeader(header); const ProxySnapshots& snapshots = snapper->getSnapshots(); for (const ProxySnapshot& snapshot : snapshots) { TableRow row; row.add(toString(snapshot.getType())); row.add(decString(snapshot.getNum())); row.add(snapshot.getType() == POST ? decString(snapshot.getPreNum()) : ""); row.add(snapshot.isCurrent() ? "" : datetime(snapshot.getDate(), utc, iso)); row.add(username(snapshot.getUid())); row.add(snapshot.getCleanup()); row.add(snapshot.getDescription()); row.add(show_userdata(snapshot.getUserdata())); table.add(row); } } break; case LM_SINGLE: { TableHeader header; header.add(_("#")); header.add(_("Date")); header.add(_("User")); header.add(_("Description")); header.add(_("Userdata")); table.setHeader(header); const ProxySnapshots& snapshots = snapper->getSnapshots(); for (const ProxySnapshot& snapshot : snapshots) { if (snapshot.getType() != SINGLE) continue; TableRow row; row.add(decString(snapshot.getNum())); row.add(snapshot.isCurrent() ? "" : datetime(snapshot.getDate(), utc, iso)); row.add(username(snapshot.getUid())); row.add(snapshot.getDescription()); row.add(show_userdata(snapshot.getUserdata())); table.add(row); } } break; case LM_PRE_POST: { TableHeader header; header.add(_("Pre #")); header.add(_("Post #")); header.add(_("Pre Date")); header.add(_("Post Date")); header.add(_("Description")); header.add(_("Userdata")); table.setHeader(header); const ProxySnapshots& snapshots = snapper->getSnapshots(); for (ProxySnapshots::const_iterator it1 = snapshots.begin(); it1 != snapshots.end(); ++it1) { if (it1->getType() != PRE) continue; ProxySnapshots::const_iterator it2 = snapshots.findPost(it1); if (it2 == snapshots.end()) continue; const ProxySnapshot& pre = *it1; const ProxySnapshot& post = *it2; TableRow row; row.add(decString(pre.getNum())); row.add(decString(post.getNum())); row.add(datetime(pre.getDate(), utc, iso)); row.add(datetime(post.getDate(), utc, iso)); row.add(pre.getDescription()); row.add(show_userdata(pre.getUserdata())); table.add(row); } } break; } cout << table; } void help_create() { cout << _(" Create snapshot:") << endl << _("\tsnapper create") << endl << endl << _(" Options for 'create' command:") << endl << _("\t--type, -t \t\tType for snapshot.") << endl << _("\t--pre-number \t\tNumber of corresponding pre snapshot.") << endl << _("\t--print-number, -p\t\tPrint number of created snapshot.") << endl << _("\t--description, -d \tDescription for snapshot.") << endl << _("\t--cleanup-algorithm, -c \tCleanup algorithm for snapshot.") << endl << _("\t--userdata, -u \tUserdata for snapshot.") << endl << _("\t--command \tRun command and create pre and post snapshots.") << endl << endl; } void command_create(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "type", required_argument, 0, 't' }, { "pre-number", required_argument, 0, 0 }, { "print-number", no_argument, 0, 'p' }, { "description", required_argument, 0, 'd' }, { "cleanup-algorithm", required_argument, 0, 'c' }, { "userdata", required_argument, 0, 'u' }, { "command", required_argument, 0, 0 }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("create", options); if (getopts.hasArgs()) { cerr << _("Command 'create' does not take arguments.") << endl; exit(EXIT_FAILURE); } enum CreateType { CT_SINGLE, CT_PRE, CT_POST, CT_PRE_POST }; const ProxySnapshots& snapshots = snapper->getSnapshots(); CreateType type = CT_SINGLE; ProxySnapshots::const_iterator snapshot1 = snapshots.end(); ProxySnapshots::const_iterator snapshot2 = snapshots.end(); bool print_number = false; SCD scd; string command; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("type")) != opts.end()) { if (opt->second == "single") type = CT_SINGLE; else if (opt->second == "pre") type = CT_PRE; else if (opt->second == "post") type = CT_POST; else if (opt->second == "pre-post") type = CT_PRE_POST; else { cerr << _("Unknown type of snapshot.") << endl; exit(EXIT_FAILURE); } } if ((opt = opts.find("pre-number")) != opts.end()) snapshot1 = snapshots.findNum(opt->second); if ((opt = opts.find("print-number")) != opts.end()) print_number = true; if ((opt = opts.find("description")) != opts.end()) scd.description = opt->second; if ((opt = opts.find("cleanup-algorithm")) != opts.end()) scd.cleanup = opt->second; if ((opt = opts.find("userdata")) != opts.end()) scd.userdata = read_userdata(opt->second); if ((opt = opts.find("command")) != opts.end()) { command = opt->second; type = CT_PRE_POST; } if (type == CT_POST && snapshot1 == snapshots.end()) { cerr << _("Missing or invalid pre-number.") << endl; exit(EXIT_FAILURE); } if (type == CT_PRE_POST && command.empty()) { cerr << _("Missing command argument.") << endl; exit(EXIT_FAILURE); } switch (type) { case CT_SINGLE: { snapshot1 = snapper->createSingleSnapshot(scd); if (print_number) cout << snapshot1->getNum() << endl; } break; case CT_PRE: { snapshot1 = snapper->createPreSnapshot(scd); if (print_number) cout << snapshot1->getNum() << endl; } break; case CT_POST: { snapshot2 = snapper->createPostSnapshot(snapshot1, scd); if (print_number) cout << snapshot2->getNum() << endl; } break; case CT_PRE_POST: { snapshot1 = snapper->createPreSnapshot(scd); system(command.c_str()); snapshot2 = snapper->createPostSnapshot(snapshot1, scd); if (print_number) cout << snapshot1->getNum() << ".." << snapshot2->getNum() << endl; } break; } } void help_modify() { cout << _(" Modify snapshot:") << endl << _("\tsnapper modify ") << endl << endl << _(" Options for 'modify' command:") << endl << _("\t--description, -d \tDescription for snapshot.") << endl << _("\t--cleanup-algorithm, -c \tCleanup algorithm for snapshot.") << endl << _("\t--userdata, -u \tUserdata for snapshot.") << endl << endl; } void command_modify(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "description", required_argument, 0, 'd' }, { "cleanup-algorithm", required_argument, 0, 'c' }, { "userdata", required_argument, 0, 'u' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("modify", options); if (!getopts.hasArgs()) { cerr << _("Command 'modify' needs at least one argument.") << endl; exit(EXIT_FAILURE); } ProxySnapshots& snapshots = snapper->getSnapshots(); while (getopts.hasArgs()) { ProxySnapshots::iterator snapshot = snapshots.findNum(getopts.popArg()); SMD smd = snapshot->getSmd(); GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("description")) != opts.end()) smd.description = opt->second; if ((opt = opts.find("cleanup-algorithm")) != opts.end()) smd.cleanup = opt->second; if ((opt = opts.find("userdata")) != opts.end()) smd.userdata = read_userdata(opt->second, snapshot->getUserdata()); snapper->modifySnapshot(snapshot, smd); } } void help_delete() { cout << _(" Delete snapshot:") << endl << _("\tsnapper delete ") << endl << endl << _(" Options for 'delete' command:") << endl << _("\t--sync, -s\t\t\tSync after deletion.") << endl << endl; } void command_delete(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "sync", no_argument, 0, 's' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("delete", options); if (!getopts.hasArgs()) { cerr << _("Command 'delete' needs at least one argument.") << endl; exit(EXIT_FAILURE); } bool sync = false; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("sync")) != opts.end()) sync = true; ProxySnapshots& snapshots = snapper->getSnapshots(); vector nums; while (getopts.hasArgs()) { string arg = getopts.popArg(); if (arg.find_first_of("-") == string::npos) { ProxySnapshots::iterator tmp = snapshots.findNum(arg); nums.push_back(tmp); } else { pair range = snapshots.findNums(arg, "-"); if (range.first->getNum() > range.second->getNum()) swap(range.first, range.second); for (unsigned int i = range.first->getNum(); i <= range.second->getNum(); ++i) { ProxySnapshots::iterator x = snapshots.find(i); if (x != snapshots.end()) { if (find_if(nums.begin(), nums.end(), [i](ProxySnapshots::iterator it) { return it->getNum() == i; }) == nums.end()) nums.push_back(x); } } } } snapper->deleteSnapshots(nums, verbose); if (sync) snapper->syncFilesystem(); } void help_mount() { cout << _(" Mount snapshot:") << endl << _("\tsnapper mount ") << endl << endl; } void command_mount(ProxySnappers* snappers, ProxySnapper* snapper) { getopts.parse("mount", GetOpts::no_options); if (!getopts.hasArgs()) { cerr << _("Command 'mount' needs at least one argument.") << endl; exit(EXIT_FAILURE); } const ProxySnapshots& snapshots = snapper->getSnapshots(); while (getopts.hasArgs()) { ProxySnapshots::const_iterator snapshot = snapshots.findNum(getopts.popArg()); snapshot->mountFilesystemSnapshot(true); } } void help_umount() { cout << _(" Umount snapshot:") << endl << _("\tsnapper umount ") << endl << endl; } void command_umount(ProxySnappers* snappers, ProxySnapper* snapper) { getopts.parse("umount", GetOpts::no_options); if (!getopts.hasArgs()) { cerr << _("Command 'umount' needs at least one argument.") << endl; exit(EXIT_FAILURE); } const ProxySnapshots& snapshots = snapper->getSnapshots(); while (getopts.hasArgs()) { ProxySnapshots::const_iterator snapshot = snapshots.findNum(getopts.popArg()); snapshot->umountFilesystemSnapshot(true); } } void help_status() { cout << _(" Comparing snapshots:") << endl << _("\tsnapper status ..") << endl << endl << _(" Options for 'status' command:") << endl << _("\t--output, -o \t\tSave status to file.") << endl << endl; } void command_status(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "output", required_argument, 0, 'o' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("status", options); if (getopts.numArgs() != 1) { cerr << _("Command 'status' needs one argument.") << endl; if (getopts.numArgs() == 2) { cerr << _("Maybe you forgot the delimiter '..' between the snapshot numbers.") << endl << _("See 'man snapper' for further instructions.") << endl; } exit(EXIT_FAILURE); } GetOpts::parsed_opts::const_iterator opt; ProxySnapshots& snapshots = snapper->getSnapshots(); pair range = snapshots.findNums(getopts.popArg()); ProxyComparison comparison = snapper->createComparison(*range.first, *range.second, false); MyFiles files(comparison.getFiles()); FILE* file = stdout; if ((opt = opts.find("output")) != opts.end()) { file = fopen(opt->second.c_str(), "w"); if (!file) { cerr << sformat(_("Opening file '%s' failed."), opt->second.c_str()) << endl; exit(EXIT_FAILURE); } } for (Files::const_iterator it = files.begin(); it != files.end(); ++it) fprintf(file, "%s %s\n", statusToString(it->getPreToPostStatus()).c_str(), it->getAbsolutePath(LOC_SYSTEM).c_str()); if (file != stdout) fclose(file); } void help_diff() { cout << _(" Comparing snapshots:") << endl << _("\tsnapper diff .. [files]") << endl << endl << _(" Options for 'diff' command:") << endl << _("\t--input, -i \t\tRead files to diff from file.") << endl << _("\t--diff-cmd \t\tCommand used for comparing files.") << endl << _("\t--extensions, -x \tExtra options passed to the diff command.") << endl << endl; } void command_diff(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "input", required_argument, 0, 'i' }, { "diff-cmd", required_argument, 0, 0 }, { "extensions", required_argument, 0, 'x' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("diff", options); if (getopts.numArgs() < 1) { cerr << _("Command 'diff' needs at least one argument.") << endl; exit(EXIT_FAILURE); } FILE* file = NULL; Differ differ; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("input")) != opts.end()) { file = fopen(opt->second.c_str(), "r"); if (!file) { cerr << sformat(_("Opening file '%s' failed."), opt->second.c_str()) << endl; exit(EXIT_FAILURE); } } if ((opt = opts.find("diff-cmd")) != opts.end()) differ.command = opt->second; if ((opt = opts.find("extensions")) != opts.end()) differ.extensions = opt->second; ProxySnapshots& snapshots = snapper->getSnapshots(); pair range = snapshots.findNums(getopts.popArg()); ProxyComparison comparison = snapper->createComparison(*range.first, *range.second, true); MyFiles files(comparison.getFiles()); files.bulk_process(file, [differ](const File& file) { differ.run(file.getAbsolutePath(LOC_PRE), file.getAbsolutePath(LOC_POST)); }); } void help_undo() { cout << _(" Undo changes:") << endl << _("\tsnapper undochange .. [files]") << endl << endl << _(" Options for 'undochange' command:") << endl << _("\t--input, -i \t\tRead files for which to undo changes from file.") << endl << endl; } void command_undo(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "input", required_argument, 0, 'i' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("undochange", options); if (getopts.numArgs() < 1) { cerr << _("Command 'undochange' needs at least one argument.") << endl; exit(EXIT_FAILURE); } ProxySnapshots& snapshots = snapper->getSnapshots(); pair range = snapshots.findNums(getopts.popArg()); FILE* file = NULL; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("input")) != opts.end()) { file = fopen(opt->second.c_str(), "r"); if (!file) { cerr << sformat(_("Opening file '%s' failed."), opt->second.c_str()) << endl; exit(EXIT_FAILURE); } } if (range.first->isCurrent()) { cerr << _("Invalid snapshots.") << endl; exit(EXIT_FAILURE); } ProxyComparison comparison = snapper->createComparison(*range.first, *range.second, true); MyFiles files(comparison.getFiles()); files.bulk_process(file, [](File& file) { file.setUndo(true); }); UndoStatistic undo_statistic = files.getUndoStatistic(); if (undo_statistic.empty()) { cout << _("nothing to do") << endl; return; } cout << sformat(_("create:%d modify:%d delete:%d"), undo_statistic.numCreate, undo_statistic.numModify, undo_statistic.numDelete) << endl; vector undo_steps = files.getUndoSteps(); for (vector::const_iterator it1 = undo_steps.begin(); it1 != undo_steps.end(); ++it1) { vector::iterator it2 = files.find(it1->name); if (it2 == files.end()) { cerr << "internal error" << endl; exit(EXIT_FAILURE); } if (it1->action != it2->getAction()) { cerr << "internal error" << endl; exit(EXIT_FAILURE); } if (verbose) { switch (it1->action) { case CREATE: cout << sformat(_("creating %s"), it2->getAbsolutePath(LOC_SYSTEM).c_str()) << endl; break; case MODIFY: cout << sformat(_("modifying %s"), it2->getAbsolutePath(LOC_SYSTEM).c_str()) << endl; break; case DELETE: cout << sformat(_("deleting %s"), it2->getAbsolutePath(LOC_SYSTEM).c_str()) << endl; break; } } if (!it2->doUndo()) { switch (it1->action) { case CREATE: cerr << sformat(_("failed to create %s"), it2->getAbsolutePath(LOC_SYSTEM).c_str()) << endl; break; case MODIFY: cerr << sformat(_("failed to modify %s"), it2->getAbsolutePath(LOC_SYSTEM).c_str()) << endl; break; case DELETE: cerr << sformat(_("failed to delete %s"), it2->getAbsolutePath(LOC_SYSTEM).c_str()) << endl; break; } } } } #ifdef ENABLE_ROLLBACK const Filesystem* getFilesystem(const ProxyConfig& config) { const map& raw = config.getAllValues(); map::const_iterator pos1 = raw.find(KEY_FSTYPE); map::const_iterator pos2 = raw.find(KEY_SUBVOLUME); if (pos1 == raw.end() || pos2 == raw.end()) { cerr << _("Failed to initialize filesystem handler.") << endl; exit(EXIT_FAILURE); } try { return Filesystem::create(pos1->second, pos2->second, target_root); } catch (const InvalidConfigException& e) { SN_CAUGHT(e); cerr << _("Failed to initialize filesystem handler.") << endl; exit(EXIT_FAILURE); } } void help_rollback() { cout << _(" Rollback:") << endl << _("\tsnapper rollback [number]") << endl << endl << _(" Options for 'rollback' command:") << endl << _("\t--print-number, -p\t\tPrint number of second created snapshot.") << endl << _("\t--description, -d \tDescription for snapshots.") << endl << _("\t--cleanup-algorithm, -c \tCleanup algorithm for snapshots.") << endl << _("\t--userdata, -u \tUserdata for snapshots.") << endl << endl; } void command_rollback(ProxySnappers* snappers, ProxySnapper* snapper) { const struct option options[] = { { "print-number", no_argument, 0, 'p' }, { "description", required_argument, 0, 'd' }, { "cleanup-algorithm", required_argument, 0, 'c' }, { "userdata", required_argument, 0, 'u' }, { 0, 0, 0, 0 } }; GetOpts::parsed_opts opts = getopts.parse("rollback", options); if (getopts.hasArgs() && getopts.numArgs() != 1) { cerr << _("Command 'rollback' takes either one or no argument.") << endl; exit(EXIT_FAILURE); } bool print_number = false; SCD scd1; scd1.description = "rollback backup"; scd1.cleanup = "number"; scd1.userdata["important"] = "yes"; SCD scd2; GetOpts::parsed_opts::const_iterator opt; if ((opt = opts.find("print-number")) != opts.end()) print_number = true; if ((opt = opts.find("description")) != opts.end()) scd1.description = scd2.description = opt->second; if ((opt = opts.find("cleanup-algorithm")) != opts.end()) scd1.cleanup = scd2.cleanup = opt->second; if ((opt = opts.find("userdata")) != opts.end()) { scd1.userdata = read_userdata(opt->second, scd1.userdata); scd2.userdata = read_userdata(opt->second); } ProxyConfig config = snapper->getConfig(); const Filesystem* filesystem = getFilesystem(config); if (filesystem->fstype() != "btrfs") { cerr << _("Command 'rollback' only available for btrfs.") << endl; exit(EXIT_FAILURE); } const string subvolume = config.getSubvolume(); if (subvolume != "/") { cerr << sformat(_("Command 'rollback' cannot be used on a non-root subvolume %s."), subvolume.c_str()) << endl; exit(EXIT_FAILURE); } pair previous_default = filesystem->getDefault(); if (previous_default.first && scd1.description == "rollback backup") scd1.description += sformat(" of #%d", previous_default.second); ProxySnapshots& snapshots = snapper->getSnapshots(); ProxySnapshots::const_iterator snapshot1 = snapshots.end(); ProxySnapshots::const_iterator snapshot2 = snapshots.end(); if (getopts.numArgs() == 0) { if (!quiet) cout << _("Creating read-only snapshot of default subvolume.") << flush; scd1.read_only = true; snapshot1 = snapper->createSingleSnapshotOfDefault(scd1); if (!quiet) cout << " " << sformat(_("(Snapshot %d.)"), snapshot1->getNum()) << endl; if (!quiet) cout << _("Creating read-write snapshot of current subvolume.") << flush; scd2.read_only = false; snapshot2 = snapper->createSingleSnapshot(snapshots.getCurrent(), scd2); if (!quiet) cout << " " << sformat(_("(Snapshot %d.)"), snapshot2->getNum()) << endl; } else { ProxySnapshots::const_iterator tmp = snapshots.findNum(getopts.popArg()); if (!quiet) cout << _("Creating read-only snapshot of current system.") << flush; snapshot1 = snapper->createSingleSnapshot(scd1); if (!quiet) cout << " " << sformat(_("(Snapshot %d.)"), snapshot1->getNum()) << endl; if (!quiet) cout << sformat(_("Creating read-write snapshot of snapshot %d."), tmp->getNum()) << flush; scd2.read_only = false; snapshot2 = snapper->createSingleSnapshot(tmp, scd2); if (!quiet) cout << " " << sformat(_("(Snapshot %d.)"), snapshot2->getNum()) << endl; } if (previous_default.first) { ProxySnapshots::iterator it = snapshots.find(previous_default.second); if (it->getCleanup().empty()) { SMD smd = it->getSmd(); smd.cleanup = "number"; snapper->modifySnapshot(it, smd); } } if (!quiet) cout << sformat(_("Setting default subvolume to snapshot %d."), snapshot2->getNum()) << endl; filesystem->setDefault(snapshot2->getNum()); Hooks::rollback(filesystem->snapshotDir(snapshot1->getNum()), filesystem->snapshotDir(snapshot2->getNum())); if (print_number) cout << snapshot2->getNum() << endl; } #endif void help_setup_quota() { cout << _(" Setup quota:") << endl << _("\tsnapper setup-quota") << endl << endl; } void command_setup_quota(ProxySnappers* snappers, ProxySnapper* snapper) { GetOpts::parsed_opts opts = getopts.parse("setup-quota", GetOpts::no_options); if (getopts.numArgs() != 0) { cerr << _("Command 'setup-quota' does not take arguments.") << endl; exit(EXIT_FAILURE); } snapper->setupQuota(); } void help_cleanup() { cout << _(" Cleanup snapshots:") << endl << _("\tsnapper cleanup ") << endl << endl; } void command_cleanup(ProxySnappers* snappers, ProxySnapper* snapper) { GetOpts::parsed_opts opts = getopts.parse("cleanup", GetOpts::no_options); if (getopts.numArgs() != 1) { cerr << _("Command 'cleanup' needs one arguments.") << endl; exit(EXIT_FAILURE); } string cleanup = getopts.popArg(); if (cleanup == "number") { do_cleanup_number(snapper, verbose); } else if (cleanup == "timeline") { do_cleanup_timeline(snapper, verbose); } else if (cleanup == "empty-pre-post") { do_cleanup_empty_pre_post(snapper, verbose); } else { cerr << sformat(_("Unknown cleanup algorithm '%s'."), cleanup.c_str()) << endl; exit(EXIT_FAILURE); } } void help_debug() { } void command_debug(ProxySnappers* snappers, ProxySnapper*) { getopts.parse("debug", GetOpts::no_options); if (getopts.hasArgs()) { cerr << _("Command 'debug' does not take arguments.") << endl; exit(EXIT_FAILURE); } for (const string& line : snappers->debug()) cout << line << endl; } #ifdef ENABLE_XATTRS void help_xa_diff() { cout << _(" Comparing snapshots extended attributes:") << endl << _("\tsnapper xadiff .. [files]") << endl << endl; } void print_xa_diff(const string loc_pre, const string loc_post) { try { XAModification xa_mod = XAModification(XAttributes(loc_pre), XAttributes(loc_post)); if (!xa_mod.empty()) { cout << "--- " << loc_pre << endl << "+++ " << loc_post << endl; xa_mod.dumpDiffReport(cout); } } catch (const XAttributesException& e) { SN_CAUGHT(e); } } void command_xa_diff(ProxySnappers* snappers, ProxySnapper* snapper) { GetOpts::parsed_opts opts = getopts.parse("xadiff", GetOpts::no_options); if (getopts.numArgs() < 1) { cerr << _("Command 'xadiff' needs at least one argument.") << endl; exit(EXIT_FAILURE); } ProxySnapshots& snapshots = snapper->getSnapshots(); pair range = snapshots.findNums(getopts.popArg()); ProxyComparison comparison = snapper->createComparison(*range.first, *range.second, true); MyFiles files(comparison.getFiles()); if (getopts.numArgs() == 0) { for (Files::const_iterator it1 = files.begin(); it1 != files.end(); ++it1) if (it1->getPreToPostStatus() & XATTRS) print_xa_diff(it1->getAbsolutePath(LOC_PRE), it1->getAbsolutePath(LOC_POST)); } else { while (getopts.numArgs() > 0) { string name = getopts.popArg(); Files::const_iterator it1 = files.findAbsolutePath(name); if (it1 == files.end()) continue; if (it1->getPreToPostStatus() & XATTRS) print_xa_diff(it1->getAbsolutePath(LOC_PRE), it1->getAbsolutePath(LOC_POST)); } } } #endif void log_do(LogLevel level, const string& component, const char* file, const int line, const char* func, const string& text) { cerr << text << endl; } bool log_query(LogLevel level, const string& component) { return level == ERROR; } void usage() __attribute__ ((__noreturn__)); void usage() { cerr << "Try 'snapper --help' for more information." << endl; exit(EXIT_FAILURE); } void help() __attribute__ ((__noreturn__)); void help(const list& cmds) { getopts.parse("help", GetOpts::no_options); if (getopts.hasArgs()) { cerr << _("Command 'help' does not take arguments.") << endl; exit(EXIT_FAILURE); } cout << _("usage: snapper [--global-options] [--command-options] [command-arguments]") << endl << endl; cout << _(" Global options:") << endl << _("\t--quiet, -q\t\t\tSuppress normal output.") << endl << _("\t--verbose, -v\t\t\tIncrease verbosity.") << endl << _("\t--utc\t\t\t\tDisplay dates and times in UTC.") << endl << _("\t--iso\t\t\t\tDisplay dates and times in ISO format.") << endl << _("\t--table-style, -t