pax_global_header00006660000000000000000000000064143525107020014511gustar00rootroot0000000000000052 comment=1b943dbc7695d62fac5c0f9549ec696a538be19c rsnapshot-1.4.5/000077500000000000000000000000001435251070200135415ustar00rootroot00000000000000rsnapshot-1.4.5/.github/000077500000000000000000000000001435251070200151015ustar00rootroot00000000000000rsnapshot-1.4.5/.github/ISSUE_TEMPLATE/000077500000000000000000000000001435251070200172645ustar00rootroot00000000000000rsnapshot-1.4.5/.github/ISSUE_TEMPLATE/1_report_bug.yml000066400000000000000000000034341435251070200224030ustar00rootroot00000000000000name: Report a bug description: Report a bug in rsnapshot labels: [bug] body: - type: checkboxes id: checklist attributes: label: Checklist description: Carefully read and work through this check list options: - label: I'm reporting a bug required: true - label: I've searched the [bugtracker](https://github.com/rsnapshot/rsnapshot/issues) for similar issues, including closed ones. required: true - type: textarea id: what-i-did attributes: label: What did you do? description: What steps should we follow to reproduce the problem? validations: required: true - type: textarea id: what-happened attributes: label: What happened? description: Describe exactly what happened validations: required: true - type: textarea id: what-i-expected attributes: label: What did you expect to happen description: What do you think _should have_ happened? validations: required: true - type: textarea id: configuration attributes: label: My configuration description: "Copy n paste your config here. This shell snippet may be useful for stripping blank lines and comments: `sed 's/ *#.*//' /path/to/rsnapshot.conf | grep .`" validations: required: true - type: textarea attributes: label: Environment description: | examples: - **server OS**: FreeBSD 12 - **snapshot_root filesystem**: ZFS value: | OS: Filesystem: render: markdown validations: required: false - type: textarea id: other-info attributes: label: Other information description: Any other useful information? placeholder: For example a snippet from your logs rsnapshot-1.4.5/.github/ISSUE_TEMPLATE/2_feature_request.yml000066400000000000000000000024171435251070200234370ustar00rootroot00000000000000name: Feature request description: Request a feature to be added or changed in rsnapshot labels: [feature] body: - type: checkboxes id: checklist attributes: label: Checklist description: Carefully read and work through this check list options: - label: I'm asking for a feature to be changed or added required: true - label: I've searched the [bugtracker](https://github.com/rsnapshot/rsnapshot/issues) for similar issues, including closed ones. required: true - label: I realise that rsnapshot is stable software and most feature requests will be rejected required: true - type: textarea id: what-i-want attributes: label: My suggestion is ... description: What would you like to see changed or added? validations: required: true - type: textarea id: why-i-want-it attributes: label: Why I want it description: Why would this be useful? validations: required: true - type: textarea id: why-in-rsnapshot attributes: label: Why should this be in rsnapshot and not implemented using some external tool? description: What other approaches have you considered and why have you rejected them? validations: required: true rsnapshot-1.4.5/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003241435251070200212530ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Get support on the mailing list url: https://sourceforge.net/p/rsnapshot/mailman/rsnapshot-discuss/ about: Join the mailing list for community support rsnapshot-1.4.5/.gitignore000066400000000000000000000005151435251070200155320ustar00rootroot00000000000000# perltidy *.messy aclocal.m4 autom4te.cache/ compile config.log config.status configure config.guess config.sub install-sh libtool ltmain.sh missing Makefile Makefile.in rsnapshot rsnapshot-*.tar.gz rsnapshot-diff rsnapshot-diff.1 rsnapshot.1 rsnapshot.conf.default t/include.ac t/*.pm t/*/*.t t/*/conf/*.conf t/support/snapshots/ rsnapshot-1.4.5/.perltidyrc000066400000000000000000000004661435251070200157310ustar00rootroot00000000000000-b -bext=.messy -ole=unix -nsfs # line-length -vmll -l=100 # use tabs for indentation -t # replace 4 spaces with tab and indent line-breaks with 2 spaces -et=4 -ci=2 # don't outdent anything -nola -noll -nolq # newline-control -bbb -nce -bar # whitespace-control -bt=2 -bbt=0 -bbvt=0 -pt=2 -pvt=2 -sbt=2 rsnapshot-1.4.5/.travis.yml000066400000000000000000000015361435251070200156570ustar00rootroot00000000000000os: - linux #- osx language: perl perl: - "5.30" - "5.28" - "5.26" - "5.24" - "5.22" - "5.20" - "5.18" - "5.16" - "5.14" install: - ./autogen.sh - ./configure before_script: # Allow us to SSH passwordless to localhost - ssh-keygen -t "rsa" -f ~/.ssh/id_rsa -N "" - cp ~/.ssh/{id_rsa.pub,authorized_keys} script: - make - make test before_deploy: - apt-get update -qq - apt-get install fakeroot -y - make tar deploy: provider: releases api_key: secure: V981ylz1rHzSd405X/QBk/ER5QUETcrLpnJBwZyV088joa2TbeHdNgQMIEhx6oLlqtJiK2To8VEk3zZ2wJwESfVp7xlm5vp0dBMpYHGUbrW13QzWdRT42gc3/CGYl4GCtcs+tIeONWehR0ubdO+TlO9L6lXes+kEXEsTTvrLS0o= file: rsnapshot-${TRAVIS_TAG}.tar.gz skip_cleanup: true on: tags: true branch: master condition: "TRAVIS_OS_NAME = linux" perl: "5.30" repo: rsnapshot/rsnapshot rsnapshot-1.4.5/AUTHORS000066400000000000000000000056761435251070200146270ustar00rootroot00000000000000Mike Rubel - http://www.mikerubel.org/computers/rsync_snapshots/ - Created the original shell scripts on which this project is based Nathan Rosenquist - Primary author and ex-maintainer of rsnapshot (until 2005). Benedikt Heine - current maintainer of rsnapshot (2015-) David Cantrell - ex-maintainer of rsnapshot (2005-2007) - Wrote the rsnapshot-diff utility David Keegel - ex-maintainer of rsnapshot (2008-2015) - Allowed remote ssh directory paths starting with "~/" as well as "/" - Fixed race condition in lock file creation, improved error reporting - Fixed a number of other bugs and buglets - Release management for rsnapshot 1.2.9, 1.3.0 Carl Wilhelm Soderstrom - Created the RPM .spec file which allowed the RPM package to be built, among other things. Ted Zlatanov - Added the one_fs feature, autoconf support, good advice, and much more. Ralf van Dooren - Added and maintains the rsnapshot entry in the FreeBSD ports tree. SlapAyoda - Provided access to his computer museum for software testing. Carl Boe - Found several subtle bugs and provided fixes for them. Shane Liebling - Fixed a compatibility bug in utils/backup_smb_share.sh Christoph Wegscheider - Added and maintains the Debian rsnapshot package Bharat Mediratta - Improved the exclusion rules to avoid backing up the snapshot root (among other things). Peter Palfrader - Enhanced error reporting to include command line options Nicolas Kaiser - Fixed typos in program and man page Chris Petersen - http://www.forevermore.net/ - Added cwrsync permanent-share support Robert Jackson - Added use_lazy_deletes option Justin Grote - Improved rsync error reporting code Anthony Ettinger - Wrote the utils/mysqlbackup.pl script - Wrote utils/rsnapshotdb Sherman Boyd - Wrote the utils/random_file_verify.sh script William Bear - Wrote the utils/rsnapreport.pl script (pretty summary of rsync stats) Eric Anderson - Improvements to utils/rsnapreport.pl. Alan Batie - Bug fixes for include_conf Dieter Bloms - Multi-line configuration options Henning Moll - stop_on_stale_lockfile Ben Low - Added support for Linux LVM snapshots David Grant - Added support for retrying rsync "rsync_numtries" number of times Matt McCutchen - Wrote rsnapshot-copy (to copy snapshot roots using rsync --link-dest) Imran Chaudhry - added -H to rsnapshot-diff rsnapshot-1.4.5/CONTRIBUTING.md000066400000000000000000000142371435251070200160010ustar00rootroot00000000000000# Developer Notes This file is intended for developers and packagers of rsnapshot, not for regular users. If you want to contribute, it's a good idea to read this document. Although the file is called *contributing*, it describes the whole release and development process. ## Bug tracker The bug tracker is hosted on [Github](https://github.com/rsnapshot/rsnapshot/issues). Please don't report any issues in the tracker on Sourceforge. ## Source code control The rsnapshot source code is on [Github](https://github.com/rsnapshot/rsnapshot). Auto-generated files should not get tracked. If you need the configure-script, generate it with `./autogen.sh`. Keep in mind that you have to execute `./autoclean.sh` before you commit. ## Opening Issues If you have found a bug, open an issue-report on Github. Before you open a report, please search if there are corresponding issues already opened or whether the bug has already been fixed on the `master` branch. Please provide the rsnapshot-version, and describe how to reproduce the bug. It would be great if you could provide also a fix. ## Building rsnapshot If you checked rsnapshot out of the git-repository, you have to generate the configure-script with: $ ./autogen.sh rsnapshot uses the common triple to build software: $ ./configure $ make $ make install ## Development The `master` branch should be complete, by which we mean that there should be no half-completed features in it. Any development should be done in a separate branch, each of them containing only a single feature or bugfix. ![The branch-model in general](./contrib/branchtree.svg?raw=true) ### Coding standards Changes that do not conform to the coding standard will not be accepted. The current coding standard is primarily encapsulated in the code itself. However briefly: * Use tabs not white space. * There should be no trailing white space on any lines. * The soft line length limited should be 80 characters. ### Adding features Fork the repository and open a new branch prefixed with `feature/`. Keep the name short and descriptive. Before opening a Pull-Request against the main repository, make sure that: * you have written tests, which test the functionality * all the tests pass * your commits are logically ordered * your commits are clean If it is not the case, please rebase/revise your branch. When you're finished you can create a pull request. Your changes will then be reviewed by a team member, before they can get merged into `master`. ### Documentation We very much welcome improvements to the documentation. It lives in two places. First, the program itself is documented in this repository. Second, the website [rsnapshot.org](https://rsnapshot.org) lives in a separate [Git repository](https://github.com/rsnapshot/rsnapshot.github.io). ### Fixing Bugs Create a new branch, prefix it with `issue/` and, if available, the github issue number. (e.g. `issue/35-umount-lvm`). Add your commits to the branch. They should be logically ordered and clean. Rebase them, if neccessary. Make sure that `make test` passes. Finished? Open a pull-request! The code will get reviewed. If the review passes, a project-member will merge it onto `master` and `release-*` (see below), and will release new bugfix-versions. ### Test cases We encourage you to write a test case for your pull-request. rsnapshot lacks of a proper testsuite, so please write tests whenever you create a Pull-request touching code at the program. We can verify your changes and intentions easier. 1. Create a directory in the testsuite-folder (`t/`) with the desired test-name. And create in this also a subfolder named `conf`. 2. Copy the skelleton-file from `t/support/skel/testcase` into your testcase-folder and name it like your folder with the **ending `.t.in`**. 3. Do the same with the conf-file `t/support/skel/testconf`, but copy it into your conf-folder. Give the file the same name and with the **ending `.conf.in`**. 4. Write your tests and of course test them. A few notes on the testsuite: - Use the SysWrap-module actively. - Any file commited in the testsuite-folder ending with `.conf` or `.t` is commited or named wrong. - Let your files always end with `.in` and execute `autogen.sh` before you run your testsuite. - Always place your configuration-files into the `conf`-subfolder. - If you have got multiple tests to check, which are quite similar, use one test-file, and multiple configuration-files located in your test-folder. (Look at the cmd-post_pre-exec testcase). ## Releases and versions ### release-branches Releases should be done from branches, named for the release version, e.g. `release-1.4`. The first release of that version should be tagged `1.4.0`. Subsequent releases of that version, which should contain no changes other than bugfixes and security fixes, should also be tagged, e.g. `1.4.1`. In the end, there should be for every minor release a branch like `release-X.Y`. The sub-releases should only get tagged on their specific branches. ### release-model in practice Here is a model presented for release 1.4.0. Make sure, that you start on the master-branch and have a clean working-directory! 1. You start branching out of the master-branch - `git checkout -b release-1-4` 2. If there are necessary changes to do before release, make them and commit them now. Mind: Any auto-generated script should *not* get tracked. You should only merge the actual changes. The configure-scripts are generated later for the release. - `git add -A` - `git commit -m "Finish Release v1.4.0"` 3. tag the commit with git and push it to repo - `git tag 1.4.0` - `git push --tags` 4. Wait for Travis-CI to finish, the fully-built release is then available on the Github-releases page. Travis will execute these commands to make a release: - `./autogen.sh` - `make` - `make tar` - upload generated file by make tar to Github releases-page ### make targets * *make man*: generate the man page from POD data in rsnapshot * *make html*: generate a HTML page from POD data in rsnapshot * *make doc*: man + html * *make test*: run the testsuite * *make clean*: clean up the mess from autoconf * *make dist*: make the release-tarball rsnapshot-1.4.5/COPYING000066400000000000000000000431061435251070200146000ustar00rootroot00000000000000 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. rsnapshot-1.4.5/ChangeLog000066400000000000000000000715701435251070200153250ustar00rootroot00000000000000------------------------------------------------------------------------------ rsnapshot changelog http://www.rsnapshot.org/ ------------------------------------------------------------------------------ VERSION 1.4.5 (Dec 27 2022) ------------------------------------------------------------------------------ - Fix regression (#310) - Merge pull request #303 from rsnapshot/update-HOWTO-to-talk-about-retain-instead-of-interval - Removing legacy Docbook files, as per discussion at https://github.com/rsnapshot/rsnapshot/pull/296 - Merge pull request #296 from hoclun-rigsep/hoclun-rigsep-patch-1 - Use @prefix@ instead of hardcoded value in rsnapshot.conf.default.in comment line (#289). VERSION 1.4.4 (Jun 12 2021) ------------------------------------------------------------------------------ - Add sentence explaining rsync_long|short_args + sign to man page (ref #11) - Fix rsnapreport problems (incorrect header, fail when rsync present, fail with LVM) (#204) - Add notes about documentation, and link to the website repo - Fix for 'rsync_cleanup_after_native_cp_al() only works on directories' fail when sync_first on and cmd_cp not set (#133). Add test - Fix for rm -rf failing when the path contains ./ - Suppress noisy error from non-GNU cp on BSD-ish machines, including MacOS - Add CentOS 7 to succesfully tested to docs - Minor tidy up rel configure options --with-test-(true|false). Refs #189 (#248) - Update travis build settings (#189). Dont use m4_esyscmd_s in configure.ac (#245) - Update docs to remove dangling refs to HOWTO on rsnapshot.org. - Skip both SSH tests (rather one) if SSH doesn't work - Use perl-5.30 for tests (used in ubuntu 20.04 focal) - Lower verbose level of rsync output to 1.3.x equivalent to work with rsnapreport.pl again - Fix location of true and false binaries on macOS VERSION 1.4.3 (Nov 17 2019) ------------------------------------------------------------------------------ - Fixes and minor updates to Pod / man page. - Adds more tests. - Restore rsync error code 23 as an important warning. - Fixes shortargs appending. - Non-LVM backup source breaks after LVM backup source. - Update rsnapreport.pl utility script to work with rsync >= 3.1.0 stats and bytes changes. VERSION 1.4.2 ------------------------------------------------------------------------------ - Fix and update Travis builds. - Fix double 8 bit shift regression in #65. - Display warning, when the verbosity is set multiple times. - Changed maintainership in all files. Other minor doc updates. VERSION 1.4.1 ------------------------------------------------------------------------------ - Properly capture rsyncs exitcode. VERSION 1.4.0 ------------------------------------------------------------------------------ - Tidy the code with perltidy - Fix --rsh command line option The --rsh option of rsync got errornously quoted (Debian Bug #743310) - Define traps to unmount LVM parts after fail If you use LVM snapshots and rsnapshot fails, rsnapshot will unmount and remove the snapshot, too. - cd to home before executing a command - if($use_lazy_deletes): remove lockfile in any case - Bail on failing cmd_(pre|post)exec If one of those commands fails, rsnapshot will exit with exit code != 0 - Capture output from rsync and print/log if level is >=4 - Print rsnpashot's PID when logging to syslog, instead of the logger's PID. - make script uses pod2man instead of /usr/bin/pod2man - rsnapshot-diff: Fixed removed files reported as addition (+ mark) - Introduction of backup_exec resource for running unix commands interspersed with backup and backup_script jobs. - check for SIGPIPE, mainly in case cron fails when trying to mail (this is probably why rsnapshot dies mysteriously if it outputs when called by cron on a system which does not have mail working). - add test case for -t showing --link-dest when there is only one snapshot - if gnu_cp_al fails, test out a simple cp -al command before suggesting that the cp does not support -al. - When snapshot_root does not exist, check existence of parent directories. - include_conf `command` now strips backticks before execution (bug fix based on suggestion from David Osborn). - Ignore extraneous output from lvremove command. (patch by Tim Connors) - Update current maintainer status in Authors section of man page. (David Cantrell stepped down between 1.3.0 and 1.3.1.) - Remove some old (1.1.x -> 1.2.0) upgrade notices from man page. - Allow linux_lvm_cmd_* in config file to have options also. - Allow `backticks with options` in include_conf. - Fix inaccurate omission of --link-dest in test-mode output - rsnapshot-diff: Add -s option to show size of each file. VERSION 1.3.1 (Aug 31 2008) ------------------------------------------------------------------------------ - Fix help message mixup in lines between -c and -t. - Add more specific error messages for not currently implemented potential per backup point options, like cmd_preexec. - Allow named pipe as logfile - suggested by Patrice Levesque. - Include rsnapshot-copy script written by Matt McCutchen. - Allow `backticks` in include_conf. - Apply fix-sync_first-rollback.diff patch from Matt McCutchen (02 Apr 2008). - Fix bug with link_dest not being used on second and later backups when you have link_dest 1 and sync_first 1. (Ignore $sync_dir_was_present) - Patch from Adam Greenbaum to allow passing of ssh arguments correctly. - David Grant added rsync_numtries to rsnapshot.conf. - Applied Ben Low's Linux LVM patch. - Added stop_on_stale_lockfile, thanks to Henning Moll. - Michael Ashley's fix for filter/space problems on the rsync command line. - Remove trailing whitespace from command names in rsnapshot.conf. - Warn about extra spaces between tab and argument. - Added multi-line config options, thanks to Dieter Bloms. - The 'interval' config option is now called 'retain'. - chdir to avoid an obscure bug in some versions of GNU rm. - Changed use_lazy_deletes option to use _delete.$$ directories. - Added note about -H and hard links to docs for rsync_short_args. - Include rsnapshot-diff.1 in rpm. - Fix bug with rsnapshot sync the first time (when .sync does not exist) trying to copy hourly.0 to .sync, even if hourly.0 doesn't exist. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=413439 VERSION 1.3.0 (Oct 21 2006) ------------------------------------------------------------------------------ - Add files rsnapreport.pl Upgrading_from_1.1 rsnapshot-HOWTO.en.html to rpm. - fix a bug with removing interval.delete (with use_lazy_deletes). - rsnapshot detects the global ssh_args option being set multiple times, and gives a config error suggesting the per-backup ssh_args instead. - Move Upgrading section of INSTALL to docs/Upgrading_from_1.1. - Incorporate patch from Alan Batie to fix bugs with include_conf. - check for quoting on $rsync_long_args rather than just splitting on space. - Change rsnapshot.conf default to have lockfile enabled. - Check for stale lock files (PID not currently running). - explicitly add mode 0644 to sysopen() to create lockfile - give warning if write() or close() return 0 when writing/closing lockfile - Make prev_max_interval "not present (yet), nothing to copy" message when rotating interval.0 more visible (level 3 instead of level 4). - Add man page for rsnapshot-diff (generated from perldoc). - Updates to rsnapshot man pages (via perldoc). - Use =item in AUTHORS section - move mis-placed =back - document stale lock file detection - strongly recommend using a lock file - add a chmod to example backup script - change crontab example for weekly from 4 "weekly"s per month to 1 per week - expand on why higher intervals are done first via cron. - Change space to tab after #cmd_rsnapshot_diff in rsnapshot.conf.default.in. - In rpm patch, set cmd_rsnapshot_diff to /usr/bin/rsnapshot-diff. - Set the test scripts t/*.t.in to run from the source directory and create directory t/support/files/a if necessary. - Trim comments in README about gnu cp versions > 5.3 since rsnapshot strips trailing slashes for gnu cp with rsnapshot 1.2.9 and later. - Add pointers to HOWTO and utils/rsnapreport.pl in README. - More examples (eg: timestamp backup_script) in rsnapshot.conf.default.in. - Change "Perhaps this is not GNU cp?" error message. VERSION 1.2.9 (May 18 2006) ------------------------------------------------------------------------------ - David Keegel now responsible for release management - Added utils/rsnapreport.pl, written by William Bear. - Detect exit status 23 and 24 from rsync in rsync_cleanup_after_native_cp_al - Add $! (system error message) to various warning messages in native_cp_al - Add more examples and comments to rsnapshot.conf.default.in - In Makefile*, for "make tar", include "t" (testing) in the tar file. - Do not strip the trailing slash if it's a remote root filesystem. (This was a bug in rsnapshot 1.2.3 if you use "backup host:/ ...") - Give a more descriptive error message "cannot have a remote snapshot_root" if user tries to configure an ssh path or rsync path as snapshot_root. - Add some more description to the error "rsnapshot refuses to create snapshot_root when no_create_root is enabled", after checking whether the snapshot_root is a non-directory file, or just plain non-existent. - Changed default syslog level to 'info' instead of 'notice' - Added include_conf directive - Allowed remote ssh directory paths starting with "~/" as well as "/". - Changed rsnapshot maintainer from Nathan Rosenquist to David Cantrell - David Cantrell wrote the rsnapshot-diff utility - Chris Petersen added cwrsync permanent-share support - Robert Jackson added use_lazy_deletes - Added rollback for backup_scripts - Added rsnapshot-diff to Autoconf/Automake install/build process - Added sha1sum to utils/sign_packages.sh - Added a CVS Id to key files - Fixed exit value for some test invocations - Added 'rsnapshot diff' front-end to rsnapshot-diff - Added cmd_rsnapshot_diff - Added cmd_preexec - Added cmd_postexec - Added db prefs examples to utils/backup_mysql.sh and utils/backup_pgsql.sh - Fixed formatting with email addresses under AUTHORS section of man page - Added an extra safety check in copy_symlink() - Fixed swapped config comments about rsync_long_args in write_upgrade_config_file() - Added optional use of the CPAN Lchown module - Added safe_chown() wrapper to handle chown() calls safely - Added warning message if we can't properly lchown a symlink - Added fixes in sync_cp_src_dest() and sync_rm_dest() to properly delete a file that is being replaced with a directory - Fixed error message for dest not being specified in sync_rm_dest() - Changed skip message level to 2 in rsync_backup_point() - Added better file type checking in sync_cp_src_dest() - Removed redundant stat() calls in sync_cp_src_dest() and sync_rm_dest() - Removed trailing slashes from file paths when calling cp -al subroutines - Changed show_usage() to use here printing - Changed strip trailing slash operation in rsync_backup_point() to use strip_trailing_slash() - Fixed incorrect call to cmd_rm_rf() in handle_interval() - Fixed trailing slash on call to bail() in handle_interval() - Added sync_first feature (touches a lot of code) - When link_dest is enabled, rsnapshot will now hunt for the newest available directory to use as a link_dest target (up to the oldest directory) - When use_lazy_deletes is enabled, remove the lockfile before the final delete - Expanded on default (no args) and help messages - Added show_latest_snapshot() subroutine for shell scripts - Fixed sync_if_different() to allow alternating directories and files to be created with the same names between runs - Removed redundant line width and indent values in wrap_cmd() calls - Anthony Ettinger wrote utils/mysqlbackup.pl - Sherman Boyd wrote utils/random_file_verify.sh VERSION 1.2.3 (Aug ?? ??:?? 2005) ------------------------------------------------------------------------------ - Fixed a bug introduced in 1.2.2 when rsync is not using --relative - Fixed a bug where the snapshot_root could be included in backups if the root filesystem was being backed up and --relative was not set VERSION 1.2.2 (Aug 20 18:07 2005) ------------------------------------------------------------------------------ - David Keegel fixed a race condition in add_lockfile() - David Keegel improved error reporting when config file isn't present - Provided a workaround for a bug in some versions of rsync which caused files to not be deleted properly when the --relative flag was set. VERSION 1.2.1 (Apr 09 15:10 2005) ------------------------------------------------------------------------------ - Fixed security bug when neither cmd_cp or link_dest are enabled - Disabled chown() call in copy_symlink() - Check for symlink before all chown() calls VERSION 1.2.0 (Jan 31 21:43 2005) ------------------------------------------------------------------------------ - Turned off buffering ($|=1) - Changed default lockfile to /var/run/rsnapshot.pid, for FHS compliance - Clarified man page licensing (GPL) - Fixed is_real_local_abs_path() to handle dangling symlinks - Changed utils/backup_smb_share.sh to re-order the smbtar arguments - Added "-uroot" to utils/backup_mysql.sh example file - Changed regex in is_blank() subroutine - Changed rsync include/exclude mode to relative - Peter Palfrader enhanced error reporting to include command line options - Bharat Mediratta improved the exclusion rules to avoid backing up the snapshot root. The old way was also kept for users who can't or don't want to upgrade their destination paths in their backup points. - Bharat Mediratta added a "+" feature to the per-backup point args to allow additive include/exclude rules. - Added safe_rename() subroutine to work around a semi-obscure timestamp bug in certain Linux kernels - Clarified error message about local/relative paths in config file parsing - Added check for leading/trailing spaces in remote (ssh) paths - Added du(1) and crontab(1) to man page references - Added config.guess and config.sub for automake - Changed default destination paths for backup points in example config file for compatibility with --relative rsync flag - Added formatting fix to show one slash only when invoking backup_script with link_dest option enabled - Broke backwards compatibility for the benefit of fixing several outstanding issues. The default value for rsync_long_args is now different. - Changed add_lockfile() verbose message to "echo $$ > /path/to/lockfile.pid" which is actually what the code is doing - Added check to make sure backup_scripts can't share destination path with backup points - Added check to make sure different backup_script destinations don't overlap and don't clobber backup point destinations - Added "cmd_du" parameter to allow specifying the path to "du" - Nicolas Kaiser provided various typo fixes in the program and man page - Fixed "missing rsync/configtest ok" bug - Added config_version parameter, which is now required - All autoconf files were updated with the ones from Debian sarge - Added upgrade-config-file feature for "make upgrade" target and manual use - Added upgrade feature to RPM spec file - Added check-config-version feature for use with shell scripts, etc. - Changed version_only argument to version-only, for consistency with new options - Conditionalized configure script to only advise the user to copy the rsnapshot.conf.default file to rsnapshot.conf if it's a new installation - Added rollback feature to restore interval.0 snapshots when using link_dest - Added second option for du, to compare subdirectories or files - Added "du_args" to pass arguments to du - Relaxed cmd line option validation, but only when du is the command - Now only show command invocation for logger on verbose level 4 - Added rsync cleanup after native_cp_al() to preserve special files - Removed warning messages from native_cp_al() about special files - Modified error printing so full run string is displayed once at the top of a list of error messages - Fixed bug in old method of preventing the snapshot root from being backed up, which occurred when snapshot root was more than one level down from a backup point - Added commented out du_args to rsnapshot.conf.default.in - Added descriptive error if "du" fails VERSION 1.1.6 (Jul 05 16:35 2004) ------------------------------------------------------------------------------ - Moved the bulk of the program into smaller subroutines (finally) - show_disk_usage() now exits the program directly instead of returning a value - "0" can now be used as a valid interval name - Config error messages now wrap before 80 columns. - Moved $file_line_num global var into parse_config_file() - Split on \s+ instead of \s where appropriate - Moved $rsync_include_args and $rsync_include_file_args into parse_config_file() - Removed the $have_{cmd} global variables entirely - Took %opts out of the global namespace and into get_cmd_line_opts() - Changed a failed syslog call from an error to a warning - Removed $cwd from global namespace - Fixed bug where cp_al() commands would not be displayed in test mode - Removed redundant $done flag from file_diff() while loop - Downgraded close() file errors to warnings - Added utils/backup_dpkg.sh to backup Debian package information - Simplified the utils/ section of the Makefile under "make tar" - Now exits with an error if unknown command line flags are found - Added a check in parse_cmd_line_opts() for the config file if specified - Changed link_dest rsync error message to description instead of number - Changed description of test mode in the help cmd - Changed description of -V mode - Removed perl defaults from man page - Clarified crontab entry timing in man page - Removed trailing slashes on calls to mkpath() to fix bug with NetBSD - Removed trailing slashes on calls to rename() to fix bug with NetBSD - Removed trailing slashes on calls to rmtree() just in case - Fixed man page generation in "make tar" target - Changed -V message when there's no directory to rotate VERSION 1.1.5 (Jun 20 20:56 2004) ------------------------------------------------------------------------------ - /bin/rm is the default for deleting directories (in the default config file) - Removed redundant validation (re: intervals) in execution section - Print PID in lockfile - Added a few more comments, clarified some existing comments - Made some minor updates to the man page - Fix display formatting double-slash bug when '/' is a backup point - Changed return value when called with no cmd line args to 1 - Added a new exit code, for warnings (2) - rsync exit codes 23 and 24 make rsnapshot return a warning exit code instead of an error - Changed link_dest comment in config file to mention "cross-platform" - Added utils/debug_moving_files.sh abuse script for testing - Added 'du' option to show disk usage in snapshot_root - Moved setlocale() after begin message - Added manual link() call when we're using --link-dest and a single file - Don't call syslog in bail() if it's a test run VERSION 1.1.4 (May 16 23:44 2004) ------------------------------------------------------------------------------ - (re)added the cmd_rm parameter. rmtree() can't delete some obscure files on some systems - Added rm_rf() stub function to recursively remove things using either rmtree() or /bin/rm -rf, depending on the configuration. - Added cp and rm detection to ./configure script - Set LC_ALL locale variable to C in an attempt to fix rmtree() locale issue - Added no_create_root option to rsnapshot - Added utils/make_cvs_snapshot.sh utility script - Added utils/sign_packages.sh utility script - Added utils/rsnapshot_if_mounted.sh utility script - Standardized comment headers in utils/ scripts - Added DEBIAN/copyright file - Fixed man page to say RSNAPSHOT instead of RSNAPSHOT-PROGRAM for the title VERSION 1.1.3 (Apr 06 14:24 2004) ------------------------------------------------------------------------------ - Validating rsync_short_args, must be in format '-an', not '-a -n' - Assembling @cmd_stack list one at a time, rather than using join() - Added "don't panic" message in rotate_interval() at verbose >= 4 when no filesystem actions get performed - Fixed latent display bug in print_msg() (now that it's getting used) - Fixed "cd //" display bug when running from root directory (removed extra /) VERSION 1.1.2 (Mar 17 02:12 2004) ------------------------------------------------------------------------------ - Fixed slash formatting issue with non-ssh rsync protocol calls - Fixed a slightly inconvenient permissions issue in rsnaptar - Added support for arbitrary arguments to backup_script files VERSION 1.1.1 (Feb 11 23:24 2004) ------------------------------------------------------------------------------ - Changed some fatal errors to warnings in some recursive subroutines - Added optional GPG support to the rsnaptar utility shell script VERSION 1.1.0 (Jan 24 16:41 2004) ------------------------------------------------------------------------------ - The first interval must now have a value of at least 2 if more than one interval is being used - Now allows "/" as a backup point on the local drive - Cleaned up command formatting to consolidate slashes - Added subroutines for centralized print/logging - Added logfile and loglevel parameters to the config file - Added logging feature - Moved loglevel/verbose number validation into a seperate subroutine - Added much more error checking on the config file - Changed @snapshot_points to @backup_points for more consistent terminology - Made all "-x" executable checks also check for "-f" file - Made rsync error messages report the full path to rsync and the correct return value - Fixed minor screen formatting issue with print_cmd() subroutine where it could line wrap the first element of a command - Changed exit error code from -1 (255) to 1, to be more standard - Added include, exclude, include_file, and exclude_file params - Fixed bug where not all locally specified backup options would be parsed - Replaced calls to bail() from config parsing code with config_err() calls - Added directory traversal checks for commands - Changed indentation in print_cmd() to 4 spaces instead of 2 - Fixed bug where a file named "tmp" in the snapshot root would not be deleted properly at runtime (if sync_if_different() was used) VERSION 1.0.10 (Jan 20 00:43 2004) ------------------------------------------------------------------------------ - Added link_dest option, to use the --link-dest flag with rsync - Now checking the return values from rsync, with conditional syntax warning for --link-dest on older versions - Added additional calls to syslog, to report rsync failures - Now checking the return value of scripts called from backup_script - Conditionalized the backup_interval() and rotate_interval() subroutines to only delete the oldest interval if we're keeping more than one of that interval - Reformatted source code so it doesn't wrap (at 126 columns, or 1024x768) - Shortened output when invoked with no args or from help to fix in 80 columns - Changed rotate_interval() subroutine to simply move directories from lower intervals up instead of recursively copying hard links - Added formatting subroutine to wrap all verbose command output at 80 columns VERSION 1.0.9 (Jan 6 19:17 2004) ------------------------------------------------------------------------------ - Added backup_mysql.sh example script in utils/ to backup a MySQL database - Added backup_smb_share.sh example script in utils/ to backup an SMB share - Changed verbose settings internally to use numbers, instead of several arbitrary variables. - Added optional "verbose" field to the config file, with values 1-5 - Fixed lockfile code to not run in test mode VERSION 1.0.8 (Dec 26 12:56 2003) ------------------------------------------------------------------------------ - Added backup_script option to have rsnapshot invoke backup scripts directly - Added two example backup scripts in the utils/ directory, one for Postgres, and one for downloading the CVS root for rsnapshot - Added optional syslog support using the "logger" command - Fixed ssh_args local override for individual backup points - Added additional comments to various sections of the code - Removed redundant rsync arguments from the default - Moved add_lockfile() call so it runs later in the program VERSION 1.0.7 (Dec 19 19:22 2003) ------------------------------------------------------------------------------ - Added ssh_args parameter to pass arbitrary args to ssh - Fully integrated Autoconf build support for the program and config file - Provided more helpful error messages when the config file is not found - Fixed false "Can not remove lockfile" error when cmd_ssh is not defined but is referenced later in backup points VERSION 1.0.6 (Nov 26 21:03 2003) ------------------------------------------------------------------------------ - added \n to utime() debug message - moved all rsync calls into dynamically populated arrays - added rsync_short_args and rsync_long_args to config file - improved parse_backup_opts() validation code - Fixed "make install" vs. INSTALL bug for Mac OS X - Ted Zlatanov added Autoconf support! VERSION 1.0.5 (Nov 14 00:22 2003) ------------------------------------------------------------------------------ - Changed domain to rsnapshot.org - Added lockfile feature, with patch to enable it by default in the RPM - Updated man page to include the one_fs feature, did general clean up - Fixed '..' pattern matching bug that would exclude valid files - Updated Makefile to use "install", now allows "make tar" as a non-root user - Moved most remaining regexes into dedicated subroutines VERSION 1.0.4 (Nov 6 23:18 2003) ------------------------------------------------------------------------------ - Added one_fs feature to the config file (same as -x on the command line) - Fixed anonymous rsync feature, which had been broken in 1.0.3 - Added verification data to the RPM package VERSION 1.0.3 (Nov 2 14:43 2003) ------------------------------------------------------------------------------ - Carl Wilhelm Soderstrom added RPM format to release. - Added "one filesystem" option to prevent rsnapshot from crossing filesystems within a backup point. Patch provided by Ted Zlatanov - Minor typographical errors fixed. VERSION 1.0.2 (Oct 24 23:22 2003) ------------------------------------------------------------------------------ - added -V "extra verbose" option, to show rsync verbose output VERSION 1.0.1 (Oct 18 03:22 2003) ------------------------------------------------------------------------------ - Make sure the snapshot directory either needs to be created or already is a directory. Previously we were checking only if the snapshot root existed, which would cause problems if it were a file. - Cleaned up and streamlined the config file parsing portion of the code. - Changed numeric comparisons so that a typo would prevent compilation, instead of acting weird. i.e. (0 == $foo) instead of ($foo == 0) VERSION 1.0.0 (Sep 14 02:58 2003) ------------------------------------------------------------------------------ - Removed backward compatibility cruft for mkdir, touch, rm, and mv. - rsnapshot has now been tested successfully on Debian and Redhat Linux, as well as IRIX and Solaris. - Man pages are no longer gzipped, for compatibility with Solaris and others. VERSION 0.9.5 (Sep 9 20:11 2003) ------------------------------------------------------------------------------ - Removed most dependencies on external shell programs. - Added compatibility for non-Linux platforms. - Did significant testing/debugging to ensure compatibility with perl 5.004, 5.6, and 5.8 - Tested and working successfully on IRIX, more tests to follow. VERSION 0.9.4 (Sep 1 23:55 2003) ------------------------------------------------------------------------------ - Heavily commented variables, and added much stricter syntax checking of the config file. - Additional error checking on mkdir - updated the man page. VERSION 0.9.3 (Sep 1 01:46 2003) ------------------------------------------------------------------------------ - Anonymous rsync connections now supported. - Reverted back from recycling the oldest snapshots to the tried and true method of deleting the old ones. - Now rsnapshot will not only create the snapshot root directory as it did before, but it will also chmod 0700 the directory when it creates it. VERSION 0.9.2 (Aug 31 04:08 2003) ------------------------------------------------------------------------------ - Optimized rotating snapshot intervals by recycling the last directory to the first, instead of deleting the last and copying .0 to .1 - Added support for spaces in path names. - The config file now requires tabs to seperate all entries, where spaces were before acceptable. This was the easiest way to support spaces in path names. - Directory entries now require a trailing slash. This is so that files can be treated correctly without having to be examined directly (which is impractical over rsync/ssh). rsnapshot-1.4.5/INSTALL.md000066400000000000000000000111521435251070200151710ustar00rootroot00000000000000# INSTALLATION rsnapshot is a filesystem snapshot utility. It uses rsync to take snapshots of local and remote filesystems for any number of machines, and then rotates the snapshots according to your rsnapshot.conf. rsnapshot comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. ## QUICK START If you are installing for the first time (and just want the defaults): * Run these commands for a quick installation from source code: (skip these commands if you have installed rpm or debian package) $ ./autogen.sh # Generates the configure script. $ ./configure --sysconfdir=/etc $ sudo make install $ sudo cp /etc/rsnapshot.conf.default /etc/rsnapshot.conf * Open up /etc/rsnapshot.conf with a text editor, and modify it for your system. * Make sure the config file syntax is valid (remember, tabs, not spaces): $ /usr/local/bin/rsnapshot configtest The rsnapshot man page installed with this software covers setup and all configuration options in detail. The [rsnapshot HOWTO](docs/HOWTOs/rsnapshot-HOWTO.en.html) may also be useful reading for first time setups. ## UPGRADING If you are upgrading from a previous installation of rsnapshot 1.1.x, please read the file: `docs/Upgrading_from_1.1` There are no special instructions for upgrading from rsnapshot 1.2.x to 1.3.x, since both use `config_version 1.2`. If you are not sure whether you need to do anything to upgrade your old rsnapshot.conf, you can run $ make upgrade or $ rsnapshot upgrade-config-file or $ rsnapshot -c /etc/rsnapshot.conf upgrade-config-file ## ADDITIONAL OPTIONS If you require more precise control over the locations of files: You can pass the following options to ./configure for more control over where various parts of rsnapshot are installed. The example values shown also happen to be the defaults. * --prefix=/usr/local This will install everything under /usr/local * --sysconfdir=/usr/local/etc This will install the example config file (rsnapshot.conf.default) under /usr/local/etc. This will also be the default directory where rsnapshot looks for its config file. It is recommended that you copy rsnapshot.conf.default and use it as a basis for the actual config file (rsnapshot.conf). * --bindir=/usr/local/bin This will install the rsnapshot program under /usr/local/bin * --mandir=/usr/local/man This will install the man page under /usr/local/man * --with-perl=/usr/bin/perl Specify your preferred path to perl. If you don't specify this, the build process will detect the first version of perl it finds in your path. * --with-rsync=/usr/bin/rsync Specify your preferred path to rsync. If you don't specify this, the build process will detect the first version of rsync it finds in your path. You can always change this later by editing the config file (rsnapshot.conf). * --with-cp=/bin/cp Specify the path to GNU cp. The traditional UNIX cp command is not sufficient. If you don't specify this, the build process will detect the first version of cp it finds in your path. If you don't have the GNU version of cp, leave this commented out in the config file (rsnapshot.conf). * --with-rm=/bin/rm Specify the path to the rm command. If you don't specify this, the build process will detect the first version of rm it finds in your path. * --with-ssh=/usr/bin/ssh Specify your preferred path to ssh. If you don't specify this, the build process will detect the first version of ssh it finds in your path. SSH is an optional feature, so it is OK if it isn't on your system. Either way, if you want to use ssh, you need to specifically enable this feature by uncommenting the "cmd_ssh" parameter in the config file (rsnapshot.conf). * --with-logger=/usr/bin/logger Specify your preferred path to logger. If you don't specify this, the build process will detect the first version of logger it finds in your path. If you want syslog support, make sure this is enabled in the config file. Syslog support is optional, so if you don't have it or comment it out it's OK. * --with-du=/usr/bin/du Specify your preferred path to du. If you don't specify this, the build process will detect the first version of du it finds in your path. The "du" command only gets used when rsnapshot is called with the "du" argument to calculate the amount of disk space used. This is optional, so if you don't have it or comment it out it's OK. rsnapshot-1.4.5/Makefile.am000066400000000000000000000050161435251070200155770ustar00rootroot00000000000000# rsnapshot version VERSION = @VERSION@ doc: man html @echo "Documentation $(man_MANS) and rsnapshot.html are now up to date." @touch doc man: $(man_MANS) rsnapshot.1 : rsnapshot @# perl 5.8 for this pod2man -c 'rsnapshot-tools' -n 'rsnapshot' -r 'rsnapshot-tools' rsnapshot > rsnapshot.1 rsnapshot-diff.1 : rsnapshot-diff @# perl 5.8 for this pod2man -c 'rsnapshot-tools' -n 'rsnapshot-diff' -r 'rsnapshot-tools' rsnapshot-diff > rsnapshot-diff.1 html: rsnapshot.html rsnapshot.html: rsnapshot pod2html rsnapshot | grep -v 'link rev' > rsnapshot.html rm -f pod2htmd.* rm -f pod2htmi.* clean: rm -rf rsnapshot-$(VERSION)/ rm -f $(man_MANS) rm -rf autom4te.cache rm -f rsnapshot-$(VERSION).tar.gz rm -f $(sysconf_DATA) rm -f rsnapshot.html rm -f pod2htmd.* rm -f pod2htmi.* rm -f Makefile config.log config.status configure.lineno rsnapshot rsnapshot-diff rm -f t/*.t rm -f t/*.pm rm -f t/support/etc/*.conf rm -f t/support/files/a/{1,2} rm -rf t/support/snapshots/*.* tar: rsnapshot-$(VERSION).tar.gz @touch tar rsnapshot-$(VERSION).tar.gz: $(man_MANS) Makefile $(bin_SCRIPTS) $(sysconf_DATA) @echo building tar file mkdir rsnapshot-$(VERSION)/ @# core files cp -a AUTHORS ChangeLog COPYING INSTALL.md Makefile.am README.md \ rsnapshot.conf.default.in rsnapshot-diff.pl rsnapshot-program.pl \ rsnapshot-$(VERSION) @# autoconf files cp -a configure.ac Makefile.am \ aclocal.m4 autom4te.cache configure install-sh Makefile.in missing \ rsnapshot-$(VERSION)/ @# documentation files cp -a docs \ rsnapshot-$(VERSION)/ @# utils cp -a utils/ \ rsnapshot-$(VERSION)/ @# testsuite cp -a t/ \ rsnapshot-$(VERSION)/ @# remove git-files find rsnapshot-$(VERSION)/ -depth -name .gitignore -exec rm -rf {} \; @# change ownership to root, and delete build dir fakeroot chown -R root:root rsnapshot-$(VERSION)/ rm -f rsnapshot-$(VERSION).tar.gz tar czf rsnapshot-$(VERSION).tar.gz rsnapshot-$(VERSION)/ rm -rf rsnapshot-$(VERSION)/ @echo # If you lack GNU make, you could use "test_cases = t/*.t" as an approximation. test_cases_in := $(wildcard t/*/*.t.in) test_cases := $(test_cases_in:%.in=%) test_configs_in := $(wildcard t/*/conf/*.conf.in) test_configs := $(test_configs_in:%.in=%) test_modules_in = $(wildcard t/*.pm.in) test_modules = $(test_modules_in:%.in=%) test: rsnapshot $(test_cases) $(test_configs) $(test_modules) @PERL@ -MTest::Harness -It -e 'runtests($(test_cases:%="%",))' bin_SCRIPTS = rsnapshot rsnapshot-diff man_MANS = rsnapshot.1 rsnapshot-diff.1 sysconf_DATA = rsnapshot.conf.default rsnapshot-1.4.5/README.md000066400000000000000000000140471435251070200150260ustar00rootroot00000000000000# RSNAPSHOT [![Build Status](https://api.travis-ci.org/rsnapshot/rsnapshot.png?branch=master)](https://travis-ci.org/rsnapshot/rsnapshot) rsnapshot comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. rsnapshot is a filesystem snapshot utility based on rsync. rsnapshot makes it easy to make periodic snapshots of local machines, and remote machines over ssh. The code makes extensive use of hard links whenever possible, to greatly reduce the disk space required. It is written entirely in perl with no module dependencies, and has been tested with versions 5.004 through 5.24.3. It should work on any reasonably modern UNIX compatible OS. It has been tested successfully on the following operating systems: - Debian: 3.0 (woody), 9.9 (stretch) - Redhat: 7.x, 8.0 - RedHat Enterprise Linux: 3.0 ES, 5, 6, 7 - Fedora Core: 1, 3 - Fedora: 17, 18 - CentOS: 3, 4, 5, 6, 7 - WhiteBox Enterprise Linux 3.0 - Slackware 9.0 - SuSE: 9.0 - Gentoo Linux - FreeBSD 4.9-STABLE - OpenBSD 3.x - Solaris 8 (SPARC and x86) - Mac OS X - IRIX 6.5 If this is your first experience with rsnapshot, you may want to read the man page which will give you a detailed walk-through on how to get rsnapshot up and running and also serve as a reference of all available commands. If you are upgrading from version 1.1.6 or earlier, make sure you read the file [Upgrading from 1.1](docs/Upgrading_from_1.1). For installation or upgrade instructions please read the [INSTALL](INSTALL.md) doc. If you want to work on improving rsnapshot please read the [CONTRIBUTING](CONTRIBUTING.md) doc. If you want to ask a question or have a general discussion use the [Mailing List](https://lists.sourceforge.net/lists/listinfo/rsnapshot-discuss). ## COMPATIBILITY NOTICES (Please read) 1. Note that systems which use GNU cp version 5.9 or later will have problems with rsnapshot versions up to and including 1.2.3, if `cmd_cp` is enabled (and points at the later gnu cp). This is no longer a problem since rsnapshot 1.2.9, as it strips off trailing slashes when running cp. 2. If you have rsync version 2.5.7 or later, you may want to enable the link_dest parameter in the rsnapshot.conf file. If you are running Linux but do not have the problem above, you should enable the `cmd_cp` parameter in rsnapshot.conf (especially if you do not have link_dest enabled). Be advised that currently `link_dest` doesn't do well with unavailable hosts. Specifically, if a remote host is unavailable using `link_dest`, there will be no latest backup of that machine, and a full re-sync will be required when it becomes available. Using the other methods, the last good snapshot will be preserved, preventing the need for a re-sync. We hope to streamline this in the future. ## CONFIGURATION Once you have installed rsnapshot, you will need to configure it. The default configuration file is /etc/rsnapshot.conf, although the exact path may be different depending on how the program was installed. If this file does not exist, copy `/etc/rsnapshot.conf.default` over to `/etc/rsnapshot.conf` and edit it to suit your tastes. See the man page for the full list of configuration options. When `/etc/rsnapshot.conf` contains your chosen settings, do a quick sanity check to make sure everything is ready to go: $ rsnapshot configtest If this works, you can see essentially what will happen when you run it for real by executing the following command (where interval is `alpha`, `beta`, `etc`): $ rsnapshot -t [interval] Once you are happy with everything, the final step is to setup a cron job to automate your backups. Here is a quick example which makes backups every four hours, and beta backups for a week: 0 */4 * * * /usr/local/bin/rsnapshot alpha 50 23 * * * /usr/local/bin/rsnapshot beta In the previous example, there will be six `alpha` snapshots taken each day (at 0,4,8,12,16, and 20 hours). There will also be beta snapshots taken every night at 11:50PM. The number of snapshots that are saved depends on the "interval" settings in /etc/rsnapshot.conf. For example: interval alpha 6 This means that every time `rsnapshot alpha` is run, it will make a new snapshot, rotate the old ones, and retain the most recent six (`alpha.0` - `alpha.5`). If you prefer instead to have three levels of backups (which we'll call `beta`, `gamma` and `delta`), you might set up cron like this: 00 00 * * * /usr/local/bin/rsnapshot beta 00 23 * * 6 /usr/local/bin/rsnapshot gamma 00 22 1 * * /usr/local/bin/rsnapshot delta This specifies a `beta` rsnapshot at midnight, a `gamma` snapshot on Saturdays at 11:00pm and a `delta` rsnapshot at 10pm on the first day of each month. Note that the backups are done from the highest interval first (in this case `delta`) and go down to the lowest interval. If you are not having cron invoke the `alpha` snapshot interval, then you must also ensure that `alpha` is not listed as one of your intervals in rsnapshot.conf (for example, comment out alpha, so that `beta` becomes the lowest interval). Remember that it is only the lowest interval which actually does the rsync to back up the relevant source directories, the higher intervals just rotate snapshots around. Unless you have enabled `sync_first` in your configuration-file, in which case only the `sync` pseudo-interval does the actual rsync, and all real intervals just rotate snapshots. For the full documentation, type `man rsnapshot` once it is installed. The [HOWTO](docs/HOWTOs/rsnapshot-HOWTO.en.html) also has a detailed overview of how to install and configure rsnapshot, and things like how to set it up so users can restore their own files. If you plan on using the `backup_script` parameter in your backup scheme, take a look at the `utils/`-directory in the source distribution for several example scripts. The `utils/rsnapreport.pl` script is well worth a look. ## AUTHORS Please see the [AUTHORS](/AUTHORS) file for the complete list of contributors. rsnapshot-1.4.5/autoclean.sh000077500000000000000000000003501435251070200160510ustar00rootroot00000000000000#!/bin/sh [ -f Makefile ] && make clean rm -rf autom4te.cache rm -f {config.h.in,config.h} rm -f {Makefile.in,Makefile} rm -f config.status rm -f configure rm -f stamp* rm -f aclocal.m4 rm -f compile rm -f missing rm -f install-sh rsnapshot-1.4.5/autogen.sh000077500000000000000000000005521435251070200155440ustar00rootroot00000000000000#!/bin/sh # generate testsuite autoconf CONFIG="t/include.ac" echo > $CONFIG echo '# this part is autogenerated by autogen.sh' >> $CONFIG echo >> $CONFIG find t/ -name '*.in' | sed 's/\.in$//g' | while read line; do echo "AC_CONFIG_FILES($line)" >> $CONFIG done echo >> $CONFIG echo '# this part had been autogenerated by autogen.sh' >> $CONFIG autoreconf -i rsnapshot-1.4.5/configure.ac000066400000000000000000000174331435251070200160370ustar00rootroot00000000000000AC_INIT([rsnapshot],[m4_chomp_all(m4_esyscmd([git describe --tags --always --dirty]))],[rsnapshot-discuss@lists.sourceforge.net]) AM_INIT_AUTOMAKE([foreign]) AC_PROG_MAKE_SET AC_CONFIG_FILES(Makefile) dnl dnl get the current working directory for the regression test suite dnl CWD=`eval echo \`pwd\`` AC_SUBST(CWD, "$CWD/") dnl general test-directory AC_SUBST(TEST, "$CWD/t/") dnl snapshot_root in conf-files AC_SUBST(SNAP, "$CWD/t/support/snapshots") dnl template-source to backup AC_SUBST(TEMP, "$CWD/t/support/files/template") dnl dnl PERL CHECK (required program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(perl, [ --with-perl=PATH Specify the path to perl ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then PERL=$withval else AC_MSG_ERROR(perl not found) fi else AC_MSG_ERROR(perl is required) fi ] ) dnl if the user didn't specify a path, hunt for it if test "$PERL" = ""; then AC_PATH_PROG(PERL, perl, no) fi dnl bail out if we can't find it if test "$PERL" = "no"; then AC_MSG_ERROR(perl is required) fi dnl dnl RSYNC CHECK (required program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(rsync, [ --with-rsync=PATH Specify the path to rsync ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then RSYNC=$withval AC_SUBST(CMD_RSYNC, "cmd_rsync $RSYNC") else AC_MSG_ERROR(rsync not found) fi else AC_MSG_ERROR(rsync is required) fi ] ) dnl if the user didn't specify a path, hunt for it if test "$RSYNC" = ""; then AC_PATH_PROG(RSYNC, rsync, no) AC_SUBST(RSYNC, "$RSYNC") AC_SUBST(CMD_RSYNC, "cmd_rsync $RSYNC") fi dnl bail out if we can't find it if test "$RSYNC" = "no"; then AC_MSG_ERROR(rsync is required) fi dnl dnl CP CHECK (optional program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(cp, [ --with-cp=PATH Specify the path to cp ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then CP=$withval else AC_MSG_ERROR(cp not found) fi else CP=no fi ] ) dnl save the program for testing AC_SUBST(TEST_CP, "$CP") dnl if the user didn't specify a path, hunt for it if test "$CP" != "no"; then AC_PATH_PROG(CP, cp, no) fi dnl if we couldn't find it, provide an example if test "$CP" = "no"; then CP=/bin/cp fi dnl either way, set the cmd_cp var AC_SUBST(CMD_CP, "cmd_cp $CP") dnl dnl RM CHECK (optional program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(rm, [ --with-rm=PATH Specify the path to rm ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then RM=$withval else AC_MSG_ERROR(rm not found) fi else RM=no fi ] ) dnl if the user didn't specify a path, hunt for it if test "$RM" != "no"; then AC_PATH_PROG(RM, rm, no) fi dnl save the program for testing AC_SUBST(TEST_RM, "$RM") dnl if we couldn't find it, provide an example if test "$RM" = "no"; then RM=/bin/rm fi dnl either way, set the cmd_rm var AC_SUBST(CMD_RM, "cmd_rm $RM") dnl dnl SSH CHECK (optional program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(ssh, [ --with-ssh=PATH Specify the path to ssh ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then SSH=$withval else AC_MSG_ERROR(ssh not found) fi else SSH=no fi ] ) dnl if the user didn't specify a path, hunt for it if test "$SSH" != "no"; then AC_PATH_PROG(SSH, ssh, no) fi dnl save the program for testing AC_SUBST(TEST_SSH, "$SSH") dnl if we couldn't find it, provide an example if test "$SSH" = "no"; then SSH=/path/to/ssh fi dnl either way, set the cmd_ssh var AC_SUBST(CMD_SSH, "cmd_ssh $SSH") dnl dnl LVM COMMANDS CHECK (optional programs) dnl dnl lvcreate AC_PATH_PROG(LVCREATE, lvcreate, no) if test "$LVCREATE" = "no"; then LVCREATE=/path/to/lvcreate fi AC_SUBST(CMD_LVCREATE, "$LVCREATE") dnl lvremove AC_PATH_PROG(LVREMOVE, lvremove, no) if test "$LVREMOVE" = "no"; then LVREMOVE=/path/to/lvremove fi AC_SUBST(CMD_LVREMOVE, "$LVREMOVE") dnl mount AC_PATH_PROG(MOUNT, mount, no) if test "$MOUNT" = "no"; then MOUNT=/path/to/mount fi AC_SUBST(CMD_MOUNT, "$MOUNT") dnl umount AC_PATH_PROG(UMOUNT, umount, no) if test "$UMOUNT" = "no"; then UMOUNT=/path/to/umount fi AC_SUBST(CMD_UMOUNT, "$UMOUNT") dnl dnl LOGGER CHECK (optional program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(logger, [ --with-logger=PATH Specify the path to logger ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then LOGGER=$withval else AC_MSG_ERROR(logger not found) fi else LOGGER=no fi ] ) dnl if the user didn't specify a path, hunt for it if test "$LOGGER" != "no"; then AC_PATH_PROG(LOGGER, logger, no) fi dnl save the program for testing AC_SUBST(TEST_LOGGER, "$LOGGER") dnl if we couldn't find it, provide an example if test "$LOGGER" = "no"; then LOGGER=/path/to/logger fi dnl either way, set the cmd_logger var AC_SUBST(CMD_LOGGER, "cmd_logger $LOGGER") dnl dnl DU CHECK (optional program) dnl dnl if the user specified a path, try that first AC_ARG_WITH(du, [ --with-du=PATH Specify the path to du ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then DU=$withval else AC_MSG_ERROR(du not found) fi else DU=no fi ] ) dnl if the user didn't specify a path, hunt for it if test "$DU" != "no"; then AC_PATH_PROG(DU, du, no) fi dnl save the program for testing AC_SUBST(TEST_DU, "$DU") dnl if we couldn't find it, provide an example if test "$DU" = "no"; then DU=/path/to/du fi dnl either way, set the cmd_du var AC_SUBST(CMD_DU, "cmd_du $DU") dnl dnl TEST_SSH_USER CHECK (optional param) dnl dnl if the user specified a path, try that first AC_ARG_WITH(test-ssh-user, [ --with-test-ssh-user=USER Specify ssh user for tests ], [ if test "x$withval" != "xno"; then TEST_SSH_USER="$withval" AC_SUBST(TEST_SSH_USER, "$TEST_SSH_USER") fi ] ) dnl if the user didn't specify a test ssh user use current user if test "$TEST_SSH_USER" = ""; then AC_SUBST(TEST_SSH_USER, "$USER") fi dnl dnl TRUE CHECK (required program in tests) dnl dnl if the user specified a path, try that first AC_ARG_WITH(test-true, [ --with-test-true=PATH Specify the path to true ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then TRUE=$withval else AC_MSG_ERROR(true not found) fi else TRUE=no fi ] ) dnl if the user didn't specify a path, hunt for it if test "$TRUE" != "no"; then AC_PATH_PROG(TRUE, true, no) fi dnl if we couldn't find it, provide an example if test "$TRUE" = "no"; then TRUE=/bin/true fi dnl dnl FALSE CHECK (required program in tests) dnl dnl if the user specified a path, try that first AC_ARG_WITH(test-false, [ --with-test-false=PATH Specify the path to false ], [ if test "x$withval" != "xno"; then if test -x "$withval"; then FALSE=$withval else AC_MSG_ERROR(false not found) fi else FALSE=no fi ] ) dnl if the user didn't specify a path, hunt for it if test "$FALSE" != "no"; then AC_PATH_PROG(FALSE, false, no) fi dnl if we couldn't find it, provide an example if test "$FALSE" = "no"; then FALSE=/bin/false fi # replace perl paths AC_CONFIG_FILES(rsnapshot:rsnapshot-program.pl) AC_CONFIG_FILES(rsnapshot-diff:rsnapshot-diff.pl) dnl try to find dependent programs for the config file AC_CONFIG_FILES(rsnapshot.conf.default:rsnapshot.conf.default.in) dnl testsuite-files are following m4_include(t/include.ac) AC_OUTPUT echo "" echo "Now type \"make test\" to run the regression test suite." echo "Then type \"make install\" to install the program." echo "" if test ! -e "$RSNAPSHOT_SYSCONFDIR/rsnapshot.conf"; then echo "After rsnapshot is installed, don't forget to copy" echo "$RSNAPSHOT_SYSCONFDIR/rsnapshot.conf.default to $RSNAPSHOT_SYSCONFDIR/rsnapshot.conf" echo "" fi rsnapshot-1.4.5/contrib/000077500000000000000000000000001435251070200152015ustar00rootroot00000000000000rsnapshot-1.4.5/contrib/branchtree.svg000066400000000000000000001467331435251070200200550ustar00rootroot00000000000000 image/svg+xmlmaster Release1.4.1 Release1.4 Merge bugfix into manster and release-branch, initiate release too Start a bugfix-branch Initiate a new Minor-release create a new feature Merge feature back into master rsnapshot-1.4.5/docs/000077500000000000000000000000001435251070200144715ustar00rootroot00000000000000rsnapshot-1.4.5/docs/HOWTOs/000077500000000000000000000000001435251070200155545ustar00rootroot00000000000000rsnapshot-1.4.5/docs/HOWTOs/rsnapshot-HOWTO.en.html000066400000000000000000001717411435251070200217750ustar00rootroot00000000000000 rsnapshot HOWTO

rsnapshot HOWTO

David Cantrell

2004-01-20

Abstract

rsnapshot is a filesystem backup utility based on rsync. Using rsnapshot, it is possible to take snapshots of your filesystems at different points in time. Using hard links, rsnapshot creates the illusion of multiple full backups, while only taking up the space of one full backup plus differences. When coupled with ssh, it is possible to take snapshots of remote filesystems as well. This document is a tutorial in the installation and configuration of rsnapshot.


1. Introduction

rsnapshot is a filesystem backup utility based on rsync. Using rsnapshot, it is possible to take snapshots of your filesystems at different points in time. Using hard links, rsnapshot creates the illusion of multiple full backups, while only taking up the space of one full backup plus differences. When coupled with ssh, it is possible to take snapshots of remote filesystems as well.

rsnapshot is written in Perl, and depends on rsync. OpenSSH, GNU cp, GNU du, and the BSD logger program are also recommended, but not required. All of these should be present on most Linux systems. rsnapshot is written with the lowest common denominator in mind. It only requires at minimum Perl 5.004 and rsync. As a result of this, it works on pretty much any UNIX-like system you care to throw at it. It has been successfully tested with Perl 5.004 through 5.8.2, on Debian, Redhat, Fedora, Solaris, Mac OS X, FreeBSD, OpenBSD, NetBSD, and IRIX.

The latest version of the program and this document can always be found at http://www.rsnapshot.org/.

1.1. What you will need

At a minimum: perl, rsync

Optionally: ssh, logger, GNU cp, GNU du

Additionally, it will help if you have reasonably good sysadmin skills.

1.2. Copyright and License

This document, rsnapshot HOWTO, is copyrighted (c) 2005 by Nathan Rosenquist, with some portions (c) 2006 David Cantrell. 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. A copy of the license is available at http://www.gnu.org/copyleft/gpl.html.

1.3. Disclaimer

No liability for the contents of this document can be accepted. Use the concepts, examples and information at your own risk. There may be errors and inaccuracies, that could be damaging to your system. Proceed with caution, and although this is highly unlikely, the author(s) do not take any responsibility.

All copyrights are held by their by their respective owners, unless specifically noted otherwise. Use of a term in this document should not be regarded as affecting the validity of any trademark or service mark. Naming of particular products or brands should not be seen as endorsements.

1.4. Feedback

Feedback is most certainly welcome for this document. Send your additions, comments and criticisms to the following email address : .

2. Motivation

I originally used Mike Rubel's shell scripts to do rsync snapshots a while back. These worked very well, but there were a number of things that I wanted to improve upon. I had to write two shell scripts that were customized for my server. If I wanted to change the number of intervals stored, or the parts of the filesystem that were archived, that meant manually editing these shell scripts. If I wanted to install them on a different server with a different configuration, this meant manually editing the scripts for the new server, and hoping the logic and the sequence of operations was correct. Also, I was doing all the backups locally, on a single machine, on a single hard drive (just to protect from dumb user mistakes like deleting files). Never the less, I continued on with this system for a while, and it did work very well.

Several months later, the IDE controller on my web server failed horribly (when I typed /sbin/shutdown, it said the command was not found). I was then faced with what was in the back of my mind all along: I had not been making regular remote backups of my server, and the local backups were of no use to me since the entire drive was corrupted. The reason I had only been making sporadic, partial remote backups is that they weren't automatic and effortless. Of course, this was no one's fault but my own, but I got frustrated enough to write a tool that would make automated remote snapshots so easy that I wouldn't ever have to worry about them again. This goal has long been reached, but work on rsnapshot still continues as people submit patches, request features, and ways are found to improve the program.

3. Installation

This section will walk you through the installation of rsnapshot, step by step. This is not the only way to do it, but it is a way that works and that is well documented. Feel free to improvise if you know what you're doing.

This guide assumes you are installing rsnapshot 1.2.0 for the first time. If you are upgrading from an earlier version, please read the INSTALL file that comes with the source distribution instead.

3.1. 30 second version (for the impatient)

./configure --sysconfdir=/etc
su
make install
cp /etc/rsnapshot.conf.default /etc/rsnapshot.conf

The rest of this section is the long version.

3.2. Untar the source code package

tar xzvf rsnapshot-1.2.0.tar.gz

If you don't have GNU tar, you may have to do this in two steps instead:

gunzip rsnapshot-1.2.0.tar.gz
tar xvf rsnapshot-1.2.0.tar

3.3. Change to the source directory

cd rsnapshot-1.2.0/

3.4. Decide where you want to install

By default, the installation procedure will install all files under /usr/local. For this tutorial, this will be OK except we will install the config file under /etc.

We are assuming that rsync, ssh, logger, and du are all in your search path. If this is not the case, you can specify the path to any of these programs using the typical Autoconf --with-program=/path/to/program syntax. For example, if Perl was in /opt/bin/perl and rsync was in /home/me/bin/rsync, you could run configure like:

./configure --with-perl=/opt/bin/perl --with-rsync=/home/me/bin/rsync

3.5. Run the configure script

This will poke and prod your system to figure out where the various external programs that rsnapshot depends on live. It also generates the Makefile that we will use to install the program. The configure script accepts arguments that can be used to tell it where to install the program, and also where to find the supporting programs. For this installation, the only non-default option we want is to put the config file in the /etc directory. To do this, run this command at the shell:

./configure --sysconfdir=/etc

If all goes well, you're ready to install the program. If there was a problem, it should be descriptive. Most likely a problem would be the result of something that was required and not found (like rsync or perl). If this happens, you must figure out where the missing program is located on your system, or install it if necessary. If you know where it is but configure couldn't find it, you can specify the path using the --with-program=/path/to/program options described above.

3.6. Install the program

If you've followed these instructions so far, you will have configured rsnapshot to be installed under /usr/local, with the config file in /etc. Under these circumstances, it will be necessary to become root to install the program. Now is the time to do so. You will, of course, need the root password to do this:

su

This will prompt you for the root password.

Now, to install rsnapshot, run the following command:

make install

This will install rsnapshot with all the settings you specified in the ./configure stage. If all goes well, you will have the following files on your system:

/usr/local/bin/rsnapshot The rsnapshot program

/usr/local/man/man1/rsnapshot.1 Man page

/etc/rsnapshot.conf.default The example config file

If you decide later that you don't want rsnapshot on your system anymore, simply remove the files listed above, or run make uninstall in the same source directory you installed from. Of course, if you installed with different options, the location of these files may be different.

4. Configuration

4.1. Create the config file

In the install process, the config file is not created or installed. However, a working example is provided that you can copy. To copy the example config file into the location rsnapshot will be looking for the real config file:

cp /etc/rsnapshot.conf.default /etc/rsnapshot.conf

As a general rule, you should avoid modifying /etc/rsnapshot.conf.default, simply because it is a working example that you may wish to refer to later. Also, if you perform an upgrade, the rsnapshot.conf.default file will always be upgraded to the latest version, while your real config file will be safe out of harm's way. Please note that if you run make upgrade during an upgrade, your rsnapshot.conf may be modified slightly, and the original will then be saved in rsnapshot.conf.backup in the same directory.

4.2. Where to go for more info

The rsnapshot.conf config file is well commented, and much of it should be fairly self-explanatory. For a full reference of all the various options, please consult the rsnapshot man page. Type:

man rsnapshot

This will give you the complete documentation. However, it assumes that you already know what you're doing to a certain extent. If you just want to get something up and running, this tutorial is a better place to start. If your system can't find the man page, /usr/local/man probably isn't in your $MANPATH environmental variable. This is beyond the scope of this document, but if it isn't working for you, you can always read the newest man page on the rsnapshot web site at http://www.rsnapshot.org/

4.3. Modifying the config file

In this example, we will be using the /.snapshots/ directory to hold the filesystem snapshots. This is referred to as the snapshot root. Feel free to put this anywhere you have lots of free disk space. However, the examples in this document assume you have not changed this parameter, so you will have to substitute this in your commands if you put it somewhere else.

Also please note that fields are separated by tabs, not spaces. The reason for this is so it's easier to specify file paths with spaces in them.

4.3.1. cmd_cp

If enabled, the cmd_cp parameter should contain the path to the GNU cp program on your filesystem. If you are using Linux, be sure to uncomment this by removing the hash mark (#) in front of it. If you are using BSD, Solaris, IRIX, or most other UNIX variants, you should leave this commented out.

What makes GNU cp so special is that unlike the traditional UNIX cp, it has the ability to make recursive copies of directories as hard links.

If you don't have GNU cp, there is a subroutine in rsnapshot that somewhat approximates this functionality (although it won't support more esoteric files such as device nodes, FIFOs, sockets, etc). This gets followed up by another call to rsync, which transfers the remaining special files, if any. In this way, rsnapshot can support all file types on every platform.

The rule of thumb is that if you're on a Linux system, leave cmd_cp enabled. If you aren't on a Linux system, leave cmd_cp disabled. There are reports of GNU cp working on BSD and other non-Linux platforms, but there have also been some cases where problems have been encountered. If you enable cmd_cp on a non-Linux platform, please let the mailing list know how it worked out for you.

4.3.2. cmd_rsync

The cmd_rsync parameter must not be commented out, and it must point to a working version of rsync. If it doesn't, the program just will not work at all.

Please note that if you are using IRIX, there is another program named rsync that is different than the real rsync most people know of. If you're on an IRIX machine, you should double check this.

4.3.3. cmd_ssh

If you have ssh installed on your system, you will want to uncomment the cmd_ssh parameter. By enabling ssh, you can take snapshots of any number of remote systems. If you don't have ssh, or plan to only take snapshots of the local filesystem, you may safely leave this commented out.

4.3.4. cmd_logger

The cmd_logger parameter specifies the path to the logger program. logger is a command line interface to syslog. See the logger man page for more details. logger should be a standard part of most UNIX-like systems. It appears to have remained unchanged since about 1993, which is good for cross-platform stability. If you comment out this parameter, it will disable syslog support in rsnapshot. It is recommended that you leave this enabled.

4.3.5. cmd_du

The cmd_du parameter specifies the path to the du program. du is a command line tool that reports on disk usage. rsnapshot uses du to generate reports about the actual amount of disk space taken up, which is otherwise difficult to estimate because of all the hard links.

If you comment this out, rsnapshot will try to use the version of du it finds in your path, if possible. The GNU version of du is recommended, since it has the best selection of features, and supports the most options. The BSD version also seems to work, although it doesn't support the -h flag. Solaris du does not work at all, because it doesn't support the -c parameter.

4.3.6. link_dest

If you have rsync version 2.5.7 or later, you may want to enable this. With link_dest enabled, rsnapshot relies on rsync to create recursive hard links, overriding GNU cp in most, but not all, cases. With link_dest enabled, every single file on your system can be backed up in one pass, on any operating system. To get the most out of rsnapshot on non-Linux platforms, link_dest should be enabled. Be advised, however, that if a remote host is unavailable during a backup, rsnapshot will take an extra step and roll back the files from the previous backup. Using GNU cp, this would not be necessary.

4.3.7. retain

rsnapshot has no idea how often you want to take snapshots. Everyone's backup scheme may be different. In order to specify how much data to save, you need to tell rsnapshot which backups to keep, and how many of each. A backup, in the context of the rsnapshot config file, is just a name. These can actually be named anything (as long as it's alphanumeric, and not a reserved word), but a very common convention is to use names like hourly and daily and so on. In this example, we want to take a snapshot every four hours, or six times a day (these are the hourly backups). We also want to keep a second set, which are taken once a day, and stored for a week (or seven days). The relevant section of the config file would, in this case, look like:

retain    hourly  6
retain    daily   7

We use the retain keyword to describe how many of which level of backup to keep. interval is a deprecated synonym.

It also has some other entries, but you can either ignore them or comment them out for now.

Please note that the hourly backup is specified first. This is very important. The first retain line is assumed to be the smallest unit of time, with each additional line getting successively larger. Thus, if you add a yearly backup, it should go at the bottom, and if you add a minutes backup, it should go before hourly. It's also worth noting that the snapshots get promoted from the most first-mentioned to the later ones in sequence. In this example, the daily snapshots get pulled from the oldest hourly snapshot, not directly from the main filesystem.

4.3.8. backup

Please note that the destination paths specified here are based on the assumption that the --relative flag is being passed to rsync via the rsync_long_args parameter. If you are installing for the first time, this is the default setting. If you upgraded from a previous version, please read the INSTALL file that came with the source distribution for more information.

This is the section where you tell rsnapshot what files you actually want to back up. You put a backup parameter first, followed by the full path to the directory or network path you're backing up. The third column is the relative path you want to back up to inside the snapshot root. Let's look at an example:

backup      /etc/      localhost/

In this example, backup tells us it's a backup point. /etc/ is the full path to the directory we want to take snapshots of, and localhost/ is a directory inside the snapshot_root we're going to put them in. Using the word localhost as the destination directory is just a convention. You might also choose to use the server's fully qualified domain name instead of localhost. If you are taking snapshots of several machines on one dedicated backup server, it's a good idea to use their various hostnames as directories to keep track of which files came from which server.

In addition to full paths on the local filesystem, you can also backup remote systems using rsync over ssh. If you have ssh installed and enabled (via the cmd_ssh parameter), you can specify a path like:

backup      root@example.com:/etc/     example.com/

This behaves fundamentally the same way, but you must take a few extra things into account.

  • The ssh daemon must be running on example.com

  • You must have access to the account you specify the remote machine, in this case the root user on example.com.

  • You must have key-based logins enabled for the root user at example.com, without passphrases. If you wanted to perform backups as another user, you could specify the other user instead of root for the source (i.e. user@domain.com). Please note that allowing remote logins with no passphrase is a security risk that may or may not be acceptable in your situation. Make sure you guard access to the backup server very carefully! For more information on how to set this up, please consult the ssh man page, or a tutorial on using ssh public and private keys. You will find that the key based logins are better in many ways, not just for rsnapshot but for convenience and security in general. One thing you can do to mitigate the potential damage from a backup server breach is to create alternate users on the client machines with uid and gid set to 0, but with a more restrictive shell such as scponly.

  • This backup occurs over the network, so it may be slower. Since this uses rsync, this is most noticeable during the first backup. Depending on how much your data changes, subsequent backups should go much, much faster since rsync only sends the differences between files.

4.3.9. backup_script

With this parameter, the second column is the full path to an executable backup script, and the third column is the local path you want to store it in (just like with the "backup" parameter). For example:

backup_script      /usr/local/bin/backup_pgsql.sh       localhost/postgres/

In this example, rsnapshot will run the script /usr/local/bin/backup_pgsql.sh in a temp directory, then sync the results into the localhost/postgres/ directory under the snapshot root. You can find the backup_pgsql.sh example script in the utils/ directory of the source distribution. Feel free to modify it for your system.

Your backup script simply needs to dump out the contents of whatever it does into its current working directory. It can create as many files and/or directories as necessary, but it should not put its files in any pre-determined path. The reason for this is that rsnapshot creates a temp directory, changes to that directory, runs the backup script, and then syncs the contents of the temp directory to the local path you specified in the third column. A typical backup script would be one that archives the contents of a database. It might look like this:

#!/bin/sh
/usr/bin/mysqldump -uroot mydatabase > mydatabase.sql
/bin/chmod 644 mydatabase.sql

There are several example scripts in the utils/ directory of the rsnapshot source distribution to give you more ideas.

Make sure the destination path you specify is unique. The backup script will completely overwrite anything in the destination path, so if you tried to specify the same destination twice, you would be left with only the files from the last script. Fortunately, rsnapshot will try to prevent you from doing this when it reads the config file.

Please remember that these backup scripts will be invoked as the user running rsnapshot. In our example, this is root. Make sure your backup scripts are owned by root, and not writable by anyone else. If you fail to do this, anyone with write access to these backup scripts will be able to put commands in them that will be run as the root user. If they are malicious, they could take over your server.

4.4. Testing your config file

When you have made all your changes, you should verify that the config file is syntactically valid, and that all the supporting programs are where you think they are. To do this, run rsnapshot with the configtest argument:

rsnapshot configtest

If all is well, it should say Syntax OK. If there's a problem, it should tell you exactly what it is. Make sure your config file is using tabs and not spaces, etc.

The final step to test your configuration is to run rsnapshot in test mode. This will print out a verbose list of the things it will do, without actually doing them. To do a test run, run this command:

rsnapshot -t hourly

This tells rsnapshot to simulate an "hourly" backup. It should print out the commands it will perform when it runs for real. Please note that the test output might be slightly different than the real execution, but only because the test doesn't actually do things that may be checked for later in the program. For example, if the program will create a directory and then later test to see if that directory exists, the test run might claim that it would create the directory twice, since it didn't actually get created during the test. This should be the only type of difference you will see while running a test.

5. Automation

Now that you have your config file set up, it's time to set up rsnapshot to be run from cron. As the root user, edit root's crontab by typing:

crontab -e

You could alternately keep a crontab file that you load in, but the concepts are the same. You want to enter the following information into root's crontab:

0 */4 * * *       /usr/local/bin/rsnapshot hourly
30 23 * * *       /usr/local/bin/rsnapshot daily

It is usually a good idea to schedule the larger intervals to run a bit before the lower ones. For example, in the crontab above, notice that daily runs 30 minutes before hourly. This helps prevent race conditions where the daily would try to run before the hourly job had finished. This same strategy should be extended so that a weekly entry would run before the daily and so on.

6. How it works

We have a snapshot root under which all backups are stored. By default, this is the directory /.snapshots/. Within this directory, other directories are created for the various intervals that have been defined. In the beginning it will be empty, but once rsnapshot has been running for a week, it should look something like this:

[root@localhost]# ls -l /.snapshots/
drwxr-xr-x    7 root     root         4096 Dec 28 00:00 daily.0
drwxr-xr-x    7 root     root         4096 Dec 27 00:00 daily.1
drwxr-xr-x    7 root     root         4096 Dec 26 00:00 daily.2
drwxr-xr-x    7 root     root         4096 Dec 25 00:00 daily.3
drwxr-xr-x    7 root     root         4096 Dec 24 00:00 daily.4
drwxr-xr-x    7 root     root         4096 Dec 23 00:00 daily.5
drwxr-xr-x    7 root     root         4096 Dec 22 00:00 daily.6
drwxr-xr-x    7 root     root         4096 Dec 29 00:00 hourly.0
drwxr-xr-x    7 root     root         4096 Dec 28 20:00 hourly.1
drwxr-xr-x    7 root     root         4096 Dec 28 16:00 hourly.2
drwxr-xr-x    7 root     root         4096 Dec 28 12:00 hourly.3
drwxr-xr-x    7 root     root         4096 Dec 28 08:00 hourly.4
drwxr-xr-x    7 root     root         4096 Dec 28 04:00 hourly.5

Inside each of these directories is a full backup of that point in time. The destination directory paths you specified under the backup and backup_script parameters get stuck directly under these directories. In the example:

backup          /etc/           localhost/

The /etc/ directory will initially get backed up into /.snapshots/hourly.0/localhost/etc/

Each subsequent time rsnapshot is run with the hourly command, it will rotate the hourly.X directories, and then copy the contents of the hourly.0 directory (using hard links) into hourly.1.

When rsnapshot daily is run, it will rotate all the daily.X directories, then rename hourly.5 to daily.0.

hourly.0 will always contain the most recent snapshot, and daily.6 will always contain a snapshot from a week ago. Unless the files change between snapshots, the full backups are really just multiple hard links to the same files. Thus, if your /etc/passwd file doesn't change in a week, hourly.0/localhost/etc/passwd and daily.6/localhost/etc/passwd will literally be the same exact file. This is how rsnapshot can be so efficient on space. If the file changes at any point, the next backup will unlink the hard link in hourly.0, and replace it with a brand new file. This will now take double the disk space it did before, but it is still considerably less than it would be to have full unique copies of this file 13 times over.

Remember that if you are using different intervals than the ones in this example, the first interval listed is the one that gets updates directly from the main filesystem. All subsequently listed intervals pull from the previous intervals. For example, if you had weekly, monthly, and yearly intervals defined (in that order), the weekly ones would get updated directly from the filesystem, the monthly ones would get updated from weekly, and the yearly ones would get updated from monthly.

7. Restoring backups

When rsnapshot is first run, it will create the snapshot_root directory (/.snapshots/ by default). It assigns this directory the permissions 700, and for good reason. The snapshot root will probably contain files owned by all sorts of users on your system. If any of these files are writeable (and of course some of them will be), the users will still have write access to their files. Thus, if they can see the snapshots directly, they can modify them, and the integrity of the snapshots can not be guaranteed.

For example, if a user had write permission to the backups and accidentally ran rm -rf /, they would delete all their files in their home directory and all the files they owned in the backups!

7.1. root only

The simplest, but least flexible solution, is to simply deny non-root users access to the snapshot root altogether. The root user will still have access of course, and as with all other aspects of system administration, must be trusted not to go messing with things too much. However, by simply denying access to everyone, the root user will be the only one who can pull backups. This may or may not be desirable, depending on your situation. For a small setup or a single-user machine, this may be all you need.

7.2. All users

If users need to be able to pull their own backups, you will need to do a little extra work up front (but probably less work in the long run). The best way to do this seems to be creating a container directory for the snapshot root with 700 permissions, giving the snapshot root directory 755 permissions, and mounting the snapshot root for the users read-only. This can be done over NFS and Samba, to name two possibilities. Let's explore how to do this using NFS on a single machine:

Set the snapshot_root variable in /etc/rsnapshot.conf equal to /.private/.snapshots/

snapshot_root       /.private/.snapshots/

Create the container directory:

mkdir /.private/

Create the real snapshot root:

mkdir /.private/.snapshots/

Create the read-only snapshot root mount point:

mkdir /.snapshots/

Set the proper permissions on these new directories:

chmod 0700 /.private/
chmod 0755 /.private/.snapshots/
chmod 0755 /.snapshots/

In /etc/exports, add /.private/.snapshots/ as a read only NFS export:

/.private/.snapshots/  127.0.0.1(ro,no_root_squash)

In /etc/fstab, mount /.private/.snapshots/ read-only under /.snapshots/

localhost:/.private/.snapshots/   /.snapshots/   nfs    ro   0 0

You should now restart your NFS daemon.

Now mount the read-only snapshot root:

mount /.snapshots/

To test this, go into the /.snapshots/ directory as root. It is set to read-only, so even root shouldn't be able to write to it. As root, try:

touch /.snapshots/testfile

This should fail, citing insufficient permissions. This is what you want. It means that your users won't be able to mess with the snapshots either.

Now, all your users have to do to recover old files is go into the /.snapshots directory, select the interval they want, and browse through the filesystem until they find the files they are looking for. They can't modify anything in here because NFS will prevent them, but they can copy anything that they had read permission for in the first place. All the regular filesystem permissions are still at work, but the read-only NFS mount prevents any writes from happening.

Please note that some NFS configurations may prevent you from accessing files that are owned by root and set to only be readable by root. In this situation, you may wish to pull backups for root from the "real" snapshot root, and let non-privileged users pull from the read-only NFS mount.

8. Conclusion

If you followed the instructions in this document, you should now have rsnapshot installed and set up to perform automatic backups of your system. If it's not working, go back and trace your steps back to see if you can isolate the problem.

The amount of disk space taken up will be equal to one full backup, plus an additional copy of every file that is changed. There is also a slight disk space overhead with creating multiple hard links, but it's not very much. On my system, adding a second, completely identical 3 Gigabyte interval alongside the original one only added about 15 Megabytes.

You can use the du option to rsnapshot to generate disk usage reports. To see the sum total of all space used, try:

rsnapshot du

If you were storing backups under localhost/home/ and wanted to see how much this subdirectory takes up throughout all your backups, try this instead:

rsnapshot du localhost/home/

The latest version of this document and the rsnapshot program can always be found at http://www.rsnapshot.org/

9. More resources

Web sites

Mike Rubel's original shell scripts, upon which this project is based

http://www.mikerubel.org/computers/rsync_snapshots/

Perl

http://www.perl.org/

GNU cp and du (coreutils package)

http://www.gnu.org/software/coreutils/

rsync

http://rsync.samba.org/

OpenSSH

http://www.openssh.org/

rsnapshot

http://www.rsnapshot.org/

rsnapshot-1.4.5/docs/HOWTOs/rsnapshot-Mac-howto000066400000000000000000000377711435251070200213730ustar00rootroot00000000000000To: rsnapshot-discuss@lists.sourceforge.net From: Jonathan Guyer Date: Thu, 19 Apr 2007 11:18:06 -0400 Subject: [rsnapshot-discuss] Backing up a Mac to an external FireWire drive I've been fumbling around trying to get a satisfactory backup of my Mac laptop to an external FireWire drive. I like the design principles of rsnapshot much better than the (large) number of other backup tools (including some very expensive commercial options). There are a number of issues though, some relating to it being a Mac (I've been using a Mac since '84, so I still have and still care about things like resource forks) and some relating to it being a laptop (it's not on full-time and not reliably configured the same way all the time, so cron jobs aren't entirely reliable). After a lot of fumbling around, I've come up with a scheme that seems to back up *all* of the information I care about and which does so as automatically and painlessly as possible when I plug in my backup drive. I wrote this up for my own purposes, just so I'd remember how to get things working again when some future OS upgrade or hardware purchase inevitably breaks things, but in the hopes that it might prove useful to others, I offer it here. Any comments or corrections are more than welcome. ========================================================= What I did to back up a Mac to an external FireWire drive ========================================================= :Author: Jonathan Guyer :Contact: guyer@nist.gov :Date: 19 April 2007 :Copyright: This document has been placed in the public domain - Get, build and install ``rsync`` version 3 from `CVS `_. The ``rsync`` shipped by Apple is buggy and the fixes proposed by http://www.onthenet.com.au/~q/ rsync/ and by http://lartmaker.nl/rsync/ don't work. .. note:: If you don't care about Mac meta data (resource forks and such) then you don't need this, but are you *sure* you don't care about Mac meta data? You *are* using a Mac, right? - Edit your ``rsnapshot.conf`` file - set, e.g.:: snapshot_root /Volumes/Backup/snapshot/ and be sure to create the appropriate directory. .. note:: If you wish to `Set the backup to run automatically when a FireWire drive is mounted`_, you don't need to declare ``snapshot_root`` in the ``rsnapshot.conf`` file, but you do still need to create the appropriate directories. .. note:: The NFS protection schemes suggested in the ``rsnapshot`` documentation aren't too applicable to a Mac, but you can protect the backup directory from all but ``sudo``. This idea was suggested by `Giuliano Gavazzi `_ - Create a ``backup`` group using NetInfo Manager (not in ``/etc/groups``) containing all user accounts. (Is there a more automatic group that will accomplish this?) - Use Access Control Lists (ACLs) to secure the backup directory - Enable ACLs:: $ sudo fsaclctl -p /Volumes/Backup -e - Set desired ACLs:: $ sudo chmod +a "backup deny add_file, delete, \ add_subdirectory, delete_child, writeattr, writeextattr \ directory_inherit" /Volumes/Backup/snapshot - set:: no_create_root 1 to prevent ``rsnapshot`` from making a mess in your ``/Volumes/`` directory when the drive is not mounted. - set the path to the ``rsync`` you installed above:: cmd_rsync /usr/local/bin/rsync - set the backup intervals appropriately - pass the following arguments to ``rsync``:: rsync_short_args -a rsync_long_args --delete --numeric-ids --relative --delete- excluded --xattrs The important one here for the Mac is ``--xattrs``. If you don't care about Macish resource forks (are you sure you don't?), then you can omit this and you don't need ``rsync`` 3. - you may want to set:: one_fs 1 - exclude transitory, dangerous, and boring things:: exclude /dev/ exclude /automount/ exclude /cores/ exclude /.vol/ exclude /Volumes/ exclude .Trashes/ exclude .Trash/ exclude .TemporaryItems/ exclude .Spotlight-V100/ exclude Library/Caches/ exclude Library/Safari/Icons/ exclude /private/tmp/ exclude /private/var/vm/ exclude /private/var/tmp/ exclude /private/var/spool/ exclude /private/var/launchd/ exclude /private/var/run/ - back up everything else:: backup / path.to.machine/ Backing up to ``path.to.machine`` is arbitrary, but makes it easy to sort things out later if you back up more than one thing and ``rsnapshot`` requires you to back up somewhere. .. note:: Can you back up to ``.``? .. attention:: If your home directory is protected by FileVault, then you'll want to add:: exclude /Users/.username/ to the excludes list and:: backup /Users/username/ path.to.machine/ to the backup list, otherwise the encrypted FileVault archive will be recopied, in its entirety, and the visible ``$HOME`` directory will be empty in the backup. .. caution:: If you do this, the backup ``$HOME`` directory will not be encrypted. Appropriate physical security measures must be taken with the backup drive. Set the backup to run automatically when a FireWire drive is mounted -------------------------------------------------------------------- - Apple's `exhortation to use the disk arbitration framework `_ is somewhat less than helpful, and R. Matthew Emerson has a peculiar definition of "well-commented", but `his code `_ is a useful starting point:: // rsnapshotd // // Mac OS X daemon for detecting the mount of a backup drive and launching // rsnapshot // // Jonathan Guyer // // This code is in the public domain #include #include #include #include typedef struct { CFUUIDRef uuid; const char * snapshot_root_conf; const char * snapshot_root_dir; const char * cmd; const char ** argv; pid_t pid; } tRsnapshotContext; // Lifted from Steve Christensen on carbon-dev char* CopyCStringFromCFString(CFStringRef cfString, CFStringEncoding encoding) { CFIndex bufferSize = CFStringGetMaximumSizeForEncoding (CFStringGetLength(cfString), encoding) + 1; char* buffer = malloc(bufferSize); if (buffer != NULL) { if (!CFStringGetCString(cfString, buffer, bufferSize, encoding)) { free(buffer); buffer = NULL; } } return buffer; } void hello_disk(DADiskRef disk, void *contextP) { CFDictionaryRef diskref = DADiskCopyDescription (disk); CFUUIDRef uuid = CFDictionaryGetValue (diskref, kDADiskDescriptionVolumeUUIDKey); tRsnapshotContext * context = (tRsnapshotContext *) contextP; diskref = DADiskCopyDescription(disk); if (uuid != NULL && uuid == context->uuid) { CFURLRef pathURL = CFDictionaryGetValue (diskref, kDADiskDescriptionVolumePathKey); CFStringRef uuidStr = CFUUIDCreateString (kCFAllocatorDefault, uuid); char * uuidCStr = CopyCStringFromCFString (uuidStr, kCFStringEncodingUTF8); if (pathURL != NULL) { CFStringRef pathStr = CFURLCopyFileSystemPath (pathURL, kCFURLPOSIXPathStyle); char * path = CopyCStringFromCFString (pathStr, kCFStringEncodingUTF8); FILE * f = fopen(context- >snapshot_root_conf, "w"); syslog(LOG_NOTICE, "performing rsnapshot backup to disk %s, UUID: %s", path, uuidCStr); CFRelease(pathStr); fprintf(f, "# This file automatically generated by rsnapshotd\n"); fprintf(f, "snapshot_root\t%s/%s\n", path, context->snapshot_root_dir); fclose(f); free(path); } else { syslog(LOG_NOTICE, "performing rsnapshot backup to nameless disk, UUID: %s", uuidCStr); } free(uuidCStr); CFRelease(uuidStr); switch (context->pid = vfork()) { case 0: { // child process int err = execv(context->cmd, context- >argv); syslog(LOG_ERR, "rsnapshot backup failed to launch: %d", err); exit(1); // in case exec fails } case -1: syslog(LOG_ERR, "vfork failed"); break; default: { } } } CFRelease(diskref); } // // This handler is pointless. The disk won't unmount as long as the process is running, // // so the the process must be killed first, which means there's nothign to kill here. // // I welcome suggestions of how to do something useful with this // void goodbye_disk(DADiskRef disk, void *contextP) // { // CFDictionaryRef diskref = DADiskCopyDescription (disk); // CFUUIDRef uuid = CFDictionaryGetValue (diskref, kDADiskDescriptionVolumeUUIDKey); // tRsnapshotContext * context = (tRsnapshotContext *) contextP; // // diskref = DADiskCopyDescription(disk); // // if (uuid != NULL && uuid == context->uuid && context->pid != 0) { // kill(context->pid, 3); // printf("\n\ndisk unmounted\n"); // } // } int main (int argc, const char * argv[]) { DASessionRef session; CFStringRef uuidStr; tRsnapshotContext context; if (argc < 5) { syslog(LOG_ERR, "Usage: rsnapshotd UUID SNAPSHOT_ROOT.CONFIG SHAPSHOT_ROOT_DIR CMD [OPTION ...]"); exit(1); } uuidStr = CFStringCreateWithCString (kCFAllocatorDefault, argv[1], kCFStringEncodingUTF8); if (!uuidStr) { syslog(LOG_ERR, "Unable to create UUID string"); exit(1); } context.uuid = CFUUIDCreateFromString (kCFAllocatorDefault, uuidStr); if (!context.uuid) { syslog(LOG_ERR, "Unable to parse UUID string"); exit(1); } context.snapshot_root_conf = argv[2]; context.snapshot_root_dir = argv[3]; context.cmd = argv[4]; context.argv = &argv[4]; context.pid = -1; session = DASessionCreate(kCFAllocatorDefault); DARegisterDiskAppearedCallback(session, NULL, hello_disk, &context); // DARegisterDiskDisappearedCallback(session, NULL, goodbye_disk, &context); DASessionScheduleWithRunLoop(session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); CFRunLoopRun(); CFRelease(session); exit(0); } If you save this as, e.g., ``rsnapshotd.c``, you can build it with:: gcc -framework DiskArbitration -framework CoreFoundation rsnapshotd.c -o rsnapshotd ``rsnapshotd`` takes four required arguments, followed by any options to pass to the command: ``UUID`` The Universally Unique Identifier for the disk you wish to back up to. You can obtain the ``UUID`` by executing ``diskutil info `` (run ``diskutil list`` to find the device ID). ``/PATH/TO/snapshot_root.conf`` The (writable) location of a file to put the path of the backup disk. This is necessary. .. attention:: You must be sure to include the line:: include_conf /PATH/TO/snapshot_root.conf after (or in place of) the ``snapshot_root`` parameter in your ``rsnapshot.conf`` file. ``snapshot_root_directory`` The relative path to the backup on the backup drive associated with ``UUID``, e.g., ``snapshot/``. ``CMD`` The fully qualified path to ``rsnapshot``. .. note:: There's nothing magical about ``rsnapshot``. This daemon will launch any command with any options you supply when the appropriate disk is mounted. ``ARGS`` Arguments to send to ``rsnapshot``, e.g., ``-c``, ``/PATH/TO/rsnapshot.conf`` and ``daily``. - Rather than setting up a cron job, add a file ``net.sourceforge.rsnapshotd.plst`` to ``/Library/LaunchDaemons/``:: Label net.sourceforge.rsnapshotd ProgramArguments /PATH/TO/rsnapshotd UUID /PATH/TO/snapshot_root.conf snapshot/ /PATH/TO/rsnapshot -c /PATH/TO/rsnapshot.conf daily OnDemand RunAtLoad LowPriorityIO Nice 20 This will cause the daemon to sit quietly, waiting for the disk to mount (it will trigger if the disk is already mounted when the daemon is loaded). The ``LowPriorityIO`` and ``Nice`` keys should prevent the rsnapshot process from being too much of a resource hog when it launches (``LowPriorityIO`` is probably more important than ``Nice``). - Install the daemon by executing:: $ sudo launchctl load /Library/LaunchDaemons/ net.sourceforge.rsnapshotd.plst -- Jonathan E. Guyer, PhD Metallurgy Division National Institute of Standards and Technology rsnapshot-1.4.5/docs/HOWTOs/rsnapshot-windows-howto000066400000000000000000000263121435251070200223520ustar00rootroot00000000000000Date: Sun, 25 Feb 2007 13:34:00 -0800 (PST) From: Bruce Hohl To: Rsnapshot mailing list Using cwRsync 2.0.10 and Rsnapshot 1.2.1 to backup a WinXP computer. ---------------- A- What to use: ---------------- 1- Method #1 - connect to cwRsync server on WinXP source: Install cwRsync server on the WinXP box and define modules (rsync file share). An Rsync client will connect to the cwRsync server module on the WinXP box. Files are transferred over port 873 unencrypted. B 1-3 Except OpenSSH parts. B 4 Module definition; Add module security as desired (man rsyncd.conf). C 1 Rsnapshot config entry for connection to a cwRsync server module. E Restoring files. 2- Method #2 - connect to OpenSSH server on WinXP source: Install cwRsync server and OpenSSH server on the WinXP box. An SSH client will connect to the OpenSSH server on the WinXP box. Files will be read from a CygWin drive. cwRsync will be used for incremental file transfer. Files will be transferred over port 22 encrypted. B 1-3 Except cwRsync parts in steps 2-3. B 5 SSH key authentication C 2 Rsnapshot config entry for connection to Cygwin drive over ssh. E Restoring files. 3- Optional Features: B 6 SSH and SFTP access with passwords. D 1-2 Backing up using Rsync. Notes: - Routing: Routing over a WAN (internet) will work only if only if DNS and routers (firewalls) are configured such that the destination (pc1.mycompany.com) can be reached reachable. +++++ Alternatives: Use VPN between source and destination. Use SSH tunnel(s) to the internal source(s). Incorporate into each backup entry. Open and close tunnels with preexec and postexec. Use firewall port forward rule(s) to the internal source(s). - cwRsync Server: Under Method #2 the cwRsync server is not used. - Windows file permissions: Rsync (Rsnapshot) does not preserve Windows files permissions. So use an appropriate method to restore files - see last section. Rsync runs on the WinXP box as SvcwRsync 1007:513. When files are copied from an Rsync module the files at the destination are owned by 1007:513 with mode of 755 for directories and 644 for files. The files on the WinXP box are seen by SSH as: rwx------+ ???????? ???????? When files are copied via Rsync over SSH the files at the destination are owned by root:root with mode of 700 for directories and 700 for files. - Rsnapshot relative parameter: backup administrator@apost1.mycompany.com:/cygdrive/c/\ Documents\ and\ Settings/apost/My\ Documents apost_My_Documents/ \ rsync_long_args=--delete --numeric-ids --delete-excluded Without --relative path to backuped files is: "/home/rsnapshot/daily.0/apost_My_Documents/My Documents/..." With --relative path to backuped files is: "/home/rsnapshot/daily.0/apost_My_Documents/c/Documents and Settings/\ apost/My Documents/..." ---------------------------------------------- B- cwRsync 2.0.10 with OpenSSH setup on WinXP: ---------------------------------------------- 1- Installation: Install cwRsync_Server_2.0.10_Installer.zip on WinXP computer. Choose the Optional OpenSSH server. Service account user:pswd = SvcwRsync:Ncj97P8hO1 2- Server settings: Set Rsync and OpenSSH to start at boot: Control Panel > Admin Tools > Services For RsyncServer: Right click then click Properties. Click Start button. Set Startup = Automatic. For Openssh SSHD: Right click then click Properties. Click Start button. Set Startup = Automatic. 3- Firewall settings: Open Firewall ports needed - for Windows firewall: Control Panel > Security Center > Windows Firewall General tab: On = Y. Exceptions tab: Click "Add Port" and add follow: RsyncServer 873 TCP RsyncServer 873 UCP Secure Shell (ssh) 22 TCP Secure Shell (ssh) 22 UCP 4- Rysync [file share] module: Add a file access module in C:\Program Files\cwRsyncServer\rsyncd.conf for the directory to be made available. Restart RsyncServer after any change. The default setup allows anonymous read-only access using the rsync client. Access can be limited to specified hosts and by module specific passwords. # global parameters: use chroot = false strict modes = false hosts allow = * log file = rsyncd.log pid file = rsyncd.pid [My_Documents] path = cygdrive/c/Documents and Settings/apost/My Documents/ read only = true transfer logging = yes 5- SSH key authentication: For passwordless rsa public key access to the WinXP PC do the following. During connection ssh requests a public keys before prompting for a password. (1) On the other computer (key maker) generate a key set as follows: root@linux_box:~# ssh-keygen -t rsa -b 2048 Accept default location and press enter when promted for a password. This creates key files id_rsa and id_rsa.pub. (2) On the WinXP box add the contents of id_rsa.pub to: c:\Program Files\cwRsyncServer\home\administrator\.ssh\authorized_keys (3) On the WinXP box edit config file: c:\Program Files\cwRsyncServer\etc\sshd_config to include "StrictModes no" Now root@linux_box can ssh to administrator@WinXP_box and will connect without being prompted for a password. This allows secure automated connections. 6- SSH passwords: Optional: for ssh and sftp password connections: For ssh and sftp access using local accounts on the Windows PC copy the local Windows users and groups to the CygWin environment as follows. These steps must be repeated if a needed change is made to a local account. Start > Programs > cwRsync Server > Start a UNIX BASH Shell $ mkpasswd --local > /etc/passwd $ mkgroup --local > /etc/group Note: A partial CygWin environment is installed at C:\Program Files\cwRsyncServer\ It includes a small subset of a Unix environment including directories for bin, doc, etc, home, tmp, var. OpenSSH access: To access the Windows ssh server using a local account with a known password: Secure shell $ ssh administrator@apost1 Secure FTP $ sftp administrator@aposta1 ------------------- C- Rsnapshot setup: ------------------- 1- Method 1 - Connect to a cwRsync Server module over port 873 (unencrypted): Add the following to /etc/rsnapshot.conf on rsnapshot server. Use tab separated entries. +++++ backup rsync://apost1.mycompany.com/My_Documents/ apost1/My_Documents/ +++++ Use a /etc/crontab entry to run rsnapshot: 0 22 * * * root /usr/bin/rsnapshot daily > /dev/null 2>&1 0 22 * * * root /usr/bin/rsnapshot weekly > /dev/null 2>&1 2- Method 2 - Connect to a Cygwin drive over ssh port 22 (encrypted): cwRsync client will be used for incremental file transfer. Add the following to /etc/rsnapshot.conf on rsnapshot server. Use tab separated entries. +++++ backup administrator@apost1.mycompany.com:/cygdrive/c/\ Documents\ and\ Settings/apost/My\ Documents apost_My_Documents/ \ rsync_long_args=--delete --numeric-ids --delete-excluded +++++ Use a /etc/crontab entry to run rsnapshot: 0 22 * * * root /usr/bin/rsnapshot daily > /dev/null 2>&1 0 22 * * * root /usr/bin/rsnapshot weekly > /dev/null 2>&1 ------------------ D- Rsync commands: ------------------ 1- Method 1 - get files from cwRsync module over port 873: To see the modules available from the rsync server: # rsync apost1.mycompany.com:: My_Documents +++++ Files are accessed from rsync module "My_Documents". Anonymous connections are allowed (if so configured). Uses rsync on port 873 without encryption. +++++ command: # rsync -azSP --numeric-ids --delete apost1.mycompany.com::My_Documents \ /home/rsnapshot/My_Documents/ 2- Method 2 - get files from CygWin drive over port 22: File access via CygWin drive "/cygdrive/c/Documents and Settings/apost/My Documents" Uses encrypted ssh connection over port 22. Under this method the password for user 'administrator' must be entered unless passwordless rsa public key authentication is setup (see above). +++++ command: # rsync -azSP --numeric-ids --delete --rsh=ssh "administrator@apost1.\ mycompany.com:/cygdrive/c/Documents\ and\ Settings/apost/My\ Documents" \ /home/rsnapshot/ command (alternate form): # rsync -azSP --numeric-ids --delete -e "ssh -l administrator" "apost1.\ mycompany.com:/cygdrive/c/Documents\ and\ Settings/apost/My\ Documents" \ /home/rsnapshot/ ------------------- E- Restoring Files: ------------------- 1- Method #1 - From the backup server by the administrator: Mount the client WinXP box: # smbmunt //apost1/C$ /mnt -o username=administrator/MYCOMPANY Copy needed files from the backup server to the mounted computer: # cp "/home/workstations/daily.1/apost1/My Documents/Some File" \ "/mnt/C/Documents and Settings/apost/My Documents/ 2- Method #2 - From any computer by the administrator: SFTP into the backup box as root. Move needed files to a public file share. Have user to retrieve the files. 3- Method #3 - From source computer by the administrator: SFTP into the backup box as root. Copy needed files to the source computer. 4- Method #4 - From source computer via Samba by the user: Here is one possible approach under which the user would have read only access to the workstation backup intervals via a Samba file share at //backup1/user. a- Install Samba. Join backup1 to the domain for centralized user accounts management. Or create a local unix and samba accounts for each user. b- Set the homes shares to read only in /etc/samba/smb.conf. [homes] comment = Home Directories browseable = no writable = no c- Create home directories as needed: Example: # mkdir /home/apost d- Create symbolic links to the appropriate backup interval directories: Example: # ln -s /home/workstations/daily.0/apost /home/apost/daily.O # ln -s /home/workstations/daily.1/apost /home/apost/daily.1 # ln -s /home/workstations/daily.2/apost /home/apost/daily.2 # ln -s /home/workstations/daily.3/apost /home/apost/daily.3 # ln -s /home/workstations/daily.4/apost /home/apost/daily.4 # ln -s /home/workstations/daily.5/apost /home/apost/daily.5 # ln -s /home/workstations/daily.6/apost /home/apost/daily.6 # ln -s /home/workstations/weekly.0/apost /home/apost/weekly.O # ln -s /home/workstations/weekly.1/apost /home/apost/weekly.1 # ln -s /home/workstations/weekly.2/apost /home/apost/weekly.2 # ln -s /home/workstations/weekly.3/apost /home/apost/weekly.3 e- If rsync via ssh is used add a cmd_postexec script to the /etc/rsnapshot config to set file modes so that file acls do not block the user from reading the needed files. script: find /home/workstations/daily.0 -type d -exec chmod 777 {} \; find /home/workstations/daily.0 -type f -exec chmod 666 {} \; rsnapshot-1.4.5/docs/Upgrading_from_1.1000066400000000000000000000316131435251070200177420ustar00rootroot00000000000000 Starting with rsnapshot 1.2.0, the default value for "rsync_long_args" has changed. This is a global change, that affects which directories your backups are stored in under the snapshot root. IT IS ABSOLUTELY VITAL THAT YOU UNDERSTAND THIS SECTION, AND MAKE SURE THAT YOU UPGRADE YOUR CONFIG FILE. Here is a quick summary of what is needed to upgrade: Run the configure script with the same arguments you used for the previous installation For example: ./configure --sysconfdir=/etc configure will look at your old rsnapshot.conf file, and prompt you to upgrade. Read the message it displays, and then type: make upgrade An upgrade script will read your existing config file and make changes if necessary. If it does, the original will be saved as "rsnapshot.conf.backup" in the same directory. After the upgrade is complete, it is recommended you look at rsnapshot.conf and make sure everything is OK. Specifically, the "rsync_long_args" parameter should now be uncommented, along with a note explaining the change. Finally, to install rsnapshot, type: make install For the last step, to make sure the upgrade went OK, run: rsnapshot configtest Now rsnapshot will continue to work just as before. However, you may want to read the section below, as it gives information on how to fully update your config file to take advantage of the new features. What follows is a detailed description of the change: In previous versions of rsnapshot (before 1.2.0), the default value for "rsync_long_args" was: --delete --numeric-ids Starting with rsnapshot 1.2.0, the new default value for "rsync_long_args" is: --delete --numeric-ids --relative --delete-excluded In both the old and new versions, explicitly setting this parameter overrides the defaults. This is what the "make upgrade" script does, it manually sets this to the old default value for backwards compatibility. This change was made for a number of good reasons: rsnapshot has a feature to prevent you from accidentally backing up your snapshot root inside itself (recursively). Without this feature, it would be very easy to do something like specifying "/" as the backup point and forgetting to exclude the snapshot root. In the past, rsnapshot would avoid this by detecting the presence of the snapshot root in a backup point. If there was a conflict, the backup point would be rewritten internally, so that rsync would get called several times, once for each top level directory in the backup point, except for the one containing the snapshot root. This was not terrible, but it prevented taking full advantage of some other features that were added over time, such as "one_fs", and the include/exclude rules. For instance, if you wanted to backup your root filesystem (but only that disk partition), you couldn't really do so without figuring out which top level directories were housed on that partition, and then specifying them all as different backup points. Attempting to use the one_fs option would not have worked, since one_fs would be passed to rsync, but once for every single top level directory. Thus, if (for example) /var was housed on a different partition, it wouldn't matter since rsync was using /var as it's starting point, not "/". Additionally, in the past it was impossible to exclude a full path with rsync's include/exclude rules, regardless of what you were backing up. The best that could be done was to exclude a pattern (like "CVS"), or once again resort to listing all the top-level directories as seperate backup points surrounding anything you wanted to exclude. Now, because of the "--relative" option, it is possible to do all these things. "--delete-excluded" is nice too, since it will automatically remove things from your backups when you decide not to back them up anymore. Of course, there is a downside as well: With "--relative" enabled in "rsync_long_args", rsync treats the destination paths differently. Before, rsync would take the files from the source path and stick them directly into the destination path. Now, it takes the full source path and recreates all the parent directories in the destination path. This limits flexibility somewhat in the destination paths, since they are now tied to the namespace of the source paths. For example, something like this can no longer be done under the new system: backup /var/qmail/ localhost/email/ Before, the finished snapshot would look like this: /.snapshots/hourly.0/localhost/email/ Now, the finished snapshot will look like this: /.snapshots/hourly.0/localhost/email/var/qmail/ By explicitly setting the "rsync_long_args" parameter, you can operate rsnapshot in either of these two ways. Please be aware that if you are using an anonymous rsync server, the rules are just a little bit different. Unlike with a local filesystem or rsync over ssh, you can not rsync directly from the top level of the remote host. Instead, an rsync server has "modules" that are exported. These are essentially just top level directories. So instead of just specifying the hostname for the destination path, you should specify the module as well. Here is an example, where example.com is the rsync server, and the exported module is called "pub": Before backup rsync://example.com/pub/cvs/ example.com/pub/cvs/ After backup rsync://example.com/pub/cvs/ example.com/pub/ If you want to keep things the way they were, make sure you run "make upgrade" or otherwise set the rsync_long_args parameter to the old value. If you want to transition your destination paths over to the new way, read on. Here are some "before and after" examples from rsnapshot.conf. Each one yields identical results on the filesystem. THE OLD WAY (rsnapshot 1.1.6 and before): backup /etc/ localhost/etc/ backup /home/ localhost/home/ backup /usr/local/ localhost/usr/local/ backup root@example.com:/etc/ example.com/etc/ backup root@example.com:/var/ example.com/var/ backup rsync://example.com:/pub/cvs/ example.com/pub/cvs/ THE NEW WAY (from rsnapshot 1.2.0 on): backup /etc/ localhost/ backup /home/ localhost/ backup /usr/local/ localhost/ backup root@example.com:/etc/ example.com/ backup root@example.com:/var/ example.com/ backup rsync://example.com:/pub/cvs/ example.com/pub/ But what happens when you had an entry like this? backup /var/qmail/ localhost/email/ As you can see, there is no direct mapping from the source path to the destination. You now have essentially four choices: 1. Manually change the backup directories for individual backup points inside the snapshot root. This is probably the best method for most people to follow. For example, take this entry: backup /var/qmail/ localhost/email/ For the conversion, we are going to change it to: backup /var/qmail/ localhost/ Assume the snapshot root is "/.snapshots/", and the smallest interval is "hourly". Under the old system, these files would be backed up in this location: /.snapshots/hourly.0/localhost/email/ After we change the config file over to use --relative in rsync_long_args, the same files will now get backed up here: /.snapshots/hourly.0/localhost/var/qmail/ To make the transition seamless, we need to move this directory inside the snapshot root, and create all the parent directories, if necessary. So in this example, we do: cd /.snapshots/hourly.0/localhost/ mkdir var/ mv email/ var/qmail/ If we map all the directories over in this way, we maintain all the hard links. The only real drawback is that users will have to learn the new locations of the files to restore them. 2. Keep rsync_long_args set to the old values. Backward compatibility is maintained, but you can't take advantage of the new features. 3. Specify rsync_long_args for a particular backup point where you want to use the old method. I.E.: backup /etc/ localhost/ backup /home/ localhost/ backup /var/qmail/ localhost/email/ rsync_long_args=--delete --numeric-ids This way you get the new features except where you need to override them for certain backup points. Be very careful here because it's easy to forget what's going on. 4. Delete the latest snapshot and do a full re-sync under the new system. This is the brute force "I don't care about my old backups anyway" method. If this interests you, then perform the following steps: A. Figure out where your snapshot root is located. B. Figure out what the smallest interval is (I.E. hourly). C. Modify the config file to change your paths over to the new system. D. Manually delete the most recent snapshot directory. Assuming your snapshot root is set to "/.snapshots/", and your smallest interval is "hourly", you would delete the following directory: rm -rf /.snapshots/hourly.0/ E. Manually run rsnapshot on the lowest interval to perform a full re-sync. rsnapshot -v hourly Aside from the extra time spent on the full re-sync, the other big drawback to this method is that your snapshots will now take up the space of TWO full backups, plus incrementals. If you liked this method and were more concerned with disk space than history, you could also conceivably just delete the entire snapshot root and start over as well. Obviously this is a tactic to be used as a last resort! Finally, be aware that the backup_script parameter does NOT follow these new rules. Backup scripts still drop their files right into the destination directory specified. This makes sense when you realize that since the files came from a script, they didn't really have a source path on the filesystem to begin with. Any backup_script parameters should remain unchanged from before. Additionally, new checks have been added to rsnapshot to prevent you from accidentally wiping out your backup points later with an incorrect backup_script destination. For instance, this would nuke your backups, except that rsnapshot won't let you do it: backup /etc/ localhost/etc/ backup_script /usr/local/bin/backup_pgsql.sh localhost/etc/ This won't work either, because the backup script destination is above the backup point: backup /etc/ localhost/etc/ backup_script /usr/local/bin/backup_pgsql.sh localhost/ The correct usage would be something like this: backup /etc/ localhost/etc/ backup_script /usr/local/bin/backup_pgsql.sh localhost/pgsql/ rsnapshot-1.4.5/rsnapshot-diff.pl000077500000000000000000000211051435251070200170270ustar00rootroot00000000000000#!@PERL@ -w ############################################################################## # rsnapshot-diff # by David Cantrell # # This program calculates the differences between two directories. It is # designed to work with two different subdirectories under the rsnapshot # snapshot_root. For example: # # rsnapshot-diff /.snapshots/daily.0/ /.snapshots/daily.1/ # # http://www.rsnapshot.org/ ############################################################################## # $Id: rsnapshot-diff.pl,v 1.6 2010/08/10 13:00:15 drhyde Exp $ =head1 NAME rsnapshot-diff - a utility for comparing the disk usage of two snapshots taken by rsnapshot =cut use strict; use constant DEBUG => 0; use Getopt::Std; my $program_name = 'rsnapshot-diff'; my %opts; my $verbose = 0; my $ignore = 0; my $show_size = 0; my $result = getopts('vVhHis', \%opts); # help if ($opts{'h'}) { print qq{ $program_name [-vVHhi] dir1 dir2 $program_name shows the differences between two 'rsnapshot' backups. -h show this help -H also show "human" sizes - MB and GB as well as just bytes -i ignore symlinks, directories, and special files in verbose output -s show the size of each changed file -v be verbose -V be more verbose (mutter about unchanged files and about symlinks) dir1 the first directory to look at dir2 the second directory to look at if you want to look at directories called '-h' or '-v' pass a first parameter of '--'. $program_name always show the changes made starting from the older of the two directories. }; exit; } =head1 SYNOPSIS rsnapshot-diff [-h|vVi] dir1 dir2 =head1 DESCRIPTION rsnapshot-diff is a companion utility for rsnapshot, which traverses two parallel directory structures and calculates the difference between them. By default it is silent apart from displaying summary information at the end, but it can be made more verbose. In the summary, "added" files may very well include files which at first glance also appear at the same place in the older directory structure. However, because the files differ in some respect, they are different files. They have a different inode number. Consequently if you use -v most of its output may appear to be pairs of files with the same name being removed and added. =head1 OPTIONS =over 4 =item -h (help) Displays help information =item -H (human) Display more human-friendly numbers - as well as showing the number of bytes changed, also show MB and GB. =item -i (ignore) If verbosity is turned on, -i suppresses information about symlinks, directories, and special files. =item -s (show size) Show the size of each changed file after the + or - sign. To sort the files by decreasing size, use this option and run the output through "sort -k 2 -rn". =item -v (verbose) Be verbose. This will spit out a list of all changes as they are encountered, apart from symlink, as well as the summary at the end. =item -V (more verbose) Be more verbose - as well as listing changed files, unchanged files and symlinks will be listed too. =item dir1 and dir2 These are the only compulsory parameters, and should be the names of two directories to compare. Their order doesn't matter, rsnapshot-diff will always compare the younger to the older, so files that appear only in the older will be reported as having been removed, and files that appear only in the younger will be reported as having been added. =back =cut # verbose if ($opts{'v'}) { $verbose = 1; } # extra verbose if ($opts{'V'}) { $verbose = 2; } # ignore if ($opts{'i'}) { $ignore = 1; } # size if ($opts{'s'}) { $show_size = 1; } if(!exists($ARGV[1]) || !-d $ARGV[0] || !-d $ARGV[1]) { die("$program_name\nUsage: $program_name [-vVhi] dir1 dir2\nType $program_name -h for details\n"); } my($dirold, $dirnew) = @ARGV; my($addedfiles, $addedspace, $deletedfiles, $deletedspace) = (0, 0, 0, 0); my($addedspace_mb, $addedspace_gb, $deletedspace_mb, $deletedspace_gb) = (0, 0, 0, 0); ($dirold, $dirnew) = ($dirnew, $dirold) if(-M $dirold < -M $dirnew); # remove trailing slahes, if any $dirold =~ s/\/+$//; $dirnew =~ s/\/+$//; print "Comparing $dirold to $dirnew\n"; compare_dirs($dirold, $dirnew); $addedspace_mb = sprintf("%.2f", $addedspace / (1024 * 1024)); $addedspace_gb = sprintf("%.2f", $addedspace_mb / 1024); $deletedspace_mb = sprintf("%.2f", $deletedspace / (1024 * 1024)); $deletedspace_gb = sprintf("%.2f", $deletedspace_mb / 1024); print "Between $dirold and $dirnew:\n"; print " $addedfiles were added, taking $addedspace bytes". ($opts{H} ? " ($addedspace_mb MB, $addedspace_gb GB)" : ''). "\n"; print " $deletedfiles were removed, saving $deletedspace bytes". ($opts{H} ? " ($deletedspace_mb MB, $deletedspace_gb GB)" : ''). "\n"; sub compare_dirs { my($old, $new) = @_; opendir(OLD, $old) || die("Can't open dir $old\n"); opendir(NEW, $new) || die("Can't open dir $new\n"); my %old = map { my $fn = $old.'/'.$_; ($_, (mystat($fn))[1]) } grep { $_ ne '.' && $_ ne '..' } readdir(OLD); my %new = map { my $fn = $new.'/'.$_; ($_, (mystat($fn))[1]) } grep { $_ ne '.' && $_ ne '..' } readdir(NEW); closedir(OLD); closedir(NEW); my @added = grep { !exists($old{$_}) } keys %new; my @deleted = grep { !exists($new{$_}) } keys %old; my @changed = grep { !-d $new.'/'.$_ && exists($old{$_}) && $old{$_} != $new{$_} } keys %new; add(map { $new.'/'.$_ } @added, @changed); remove(map { $old.'/'.$_ } @deleted, @changed); if($verbose == 2) { my %changed = map { ($_, 1) } @changed, @added, @deleted; print "0 $new/$_\n" foreach(grep { !-d "$new/$_" && !exists($changed{$_}) } keys %new); } foreach (grep { !-l $new.'/'.$_ && !-l $old.'/'.$_ && -d $new.'/'.$_ && -d $old.'/'.$_ } keys %new) { print "Comparing subdirs $new/$_ and $old/$_ ...\n" if(DEBUG); compare_dirs($old.'/'.$_, $new.'/'.$_); } } sub add { my @added = @_; print "Adding ".join(', ', @added)."\n" if(DEBUG && @added); foreach(grep { !-d } @added) { $addedfiles++; my $size = (mystat($_))[7]; $addedspace += $size; # if ignore is on, only print files unless ($ignore && (-l || !-f)) { print ''.($show_size ? "+ $size $_" : "+ $_"). (-l $_ ? ' (symlink)' : ''). "\n" if($verbose == 2 || ($verbose == 1 && !-l $_)); } } foreach my $dir (grep { !-l && -d } @added) { opendir(DIR, $dir) || die("Can't open dir $dir\n"); add(map { $dir.'/'.$_ } grep { $_ ne '.' && $_ ne '..' } readdir(DIR)) } } sub remove { my @removed = @_; print "Removing ".join(', ', @removed)."\n" if(DEBUG && @removed); foreach(grep { !-d } @removed) { $deletedfiles++; my $size = (mystat($_))[7]; $deletedspace += $size; # if ignore is on, only print files unless ($ignore && (-l || !-f)) { print ''.($show_size ? "- $size $_" : "- $_"). (-l $_ ? ' (symlink)' : ''). "\n" if($verbose == 2 || ($verbose == 1 && !-l $_)); } } foreach my $dir (grep { !-l && -d } @removed) { opendir(DIR, $dir) || die("Can't open dir $dir\n"); remove(map { $dir.'/'.$_ } grep { $_ ne '.' && $_ ne '..' } readdir(DIR)) } } { my $device; sub mystat { local $_ = shift; my @stat = (-l) ? lstat() : stat(); # on first stat, memorise device $device = $stat[0] unless(defined($device)); die("Can't compare across devices.\n(looking at $_)\n") unless($device == $stat[0] || -p $_); return @stat; } } =head1 SEE ALSO rsnapshot =head1 BUGS Please report bugs (and other comments) to the rsnapshot-discuss mailing list: L =head1 AUTHOR David Cantrell Edavid@cantrell.org.ukE =head1 COPYRIGHT Copyright 2005-2010 David Cantrell =head1 LICENCE 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 =cut rsnapshot-1.4.5/rsnapshot-program.pl000077500000000000000000006543551435251070200176110ustar00rootroot00000000000000#!@PERL@ -w ######################################################################## # # # rsnapshot # # by Nathan Rosenquist # # now maintained by Benedikt Heine # # # # The official rsnapshot website is located at # # http://www.rsnapshot.org/ # # # # Copyright (C) 2003-2005 Nathan Rosenquist # # # # Portions Copyright (C) 2002-2006 Mike Rubel, Carl Wilhelm Soderstrom,# # Ted Zlatanov, Carl Boe, Shane Liebling, Bharat Mediratta, # # Peter Palfrader, Nicolas Kaiser, David Cantrell, Chris Petersen, # # Robert Jackson, Justin Grote, David Keegel, Alan Batie # # # # rsnapshot comes with ABSOLUTELY NO WARRANTY. This is free software, # # and you may copy, distribute and/or modify it under the terms of # # the GNU GPL (version 2 or at your option any later version). # # See the GNU General Public License (in file: COPYING) for details. # # # # Based on code originally by Mike Rubel # # http://www.mikerubel.org/computers/rsync_snapshots/ # # # ######################################################################## # tabstops are set to 4 spaces # in vi, do: set ts=4 sw=4 ######################################## ### STANDARD MODULES ### ######################################## require 5.004; use strict; use DirHandle; # DirHandle() use Cwd; # cwd() use Getopt::Std; # getopts() use File::Path; # mkpath(), rmtree() use File::stat; # stat(), lstat() use POSIX qw(locale_h); # setlocale() use Fcntl; # sysopen() use IO::File; # recursive open in parse_config_file use IPC::Open3 qw(open3); #open rsync with error output use IO::Handle; # handle autoflush for rsync-output ######################################## ### CPAN MODULES ### ######################################## # keep track of whether we have access to the Lchown module my $have_lchown = 0; # use_lchown() is called later, so we can log the results ######################################## ### DECLARE GLOBAL VARIABLES ### ######################################## # turn off buffering $| = 1; # version of rsnapshot my $VERSION = '@VERSION@'; # command or interval to execute (first cmd line arg) my $cmd; # default configuration file my $config_file; # hash to hold variables from the configuration file my %config_vars; # array of hash_refs containing the destination backup point # and either a source dir or a script to run my @backup_points; # array of backup points to rollback, in the event of failure my @rollback_points; # "intervals" are user defined time periods (e.g., alpha, beta) # this array holds hash_refs containing the name of the interval, # and the number of snapshots to keep of it # # NB, intervals and now called backup levels, and the config parameter # is 'retain' my @intervals; # store interval data (mostly info about which one we're on, what was before, etc.) # this is a convenient reference to some of the data from and metadata about @intervals my $interval_data_ref; # intervals can't have these values, because they're either taken by other commands # or reserved for future use my @reserved_words = qw( archive check-config-version configtest diff delete du get-latest-snapshot help history list print-config restore rollback sync upgrade-config-file version version-only ); # global flags that change the outcome of the program, # and are configurable by both cmd line and config flags # my $test = 0; # turn verbose on, but don't execute # any filesystem commands my $do_configtest = 0; # parse config file and exit my $one_fs = 0; # one file system (don't cross # partitions within a backup point) my $link_dest = 0; # use the --link-dest option to rsync my $stop_on_stale_lockfile = 0; # stop if there is a stale lockfile # how much noise should we make? the default is 2 # # 0 Absolutely quiet (reserved, but not implemented) # 1 Don't display warnings about FIFOs and special files # 2 Default (errors only) # 3 Verbose (show shell commands and equivalents) # 4 Extra verbose messages (individual actions inside some subroutines, output from rsync) # 5 Debug # # define verbose and loglevel my $verbose = undef; my $loglevel = undef; # set defaults for verbose and loglevel my $default_verbose = 2; my $default_loglevel = 3; # assume the config file is valid until we find an error my $config_perfect = 1; # exit code for rsnapshot my $exit_code = 0; # global defaults for external programs my $default_rsync_short_args = '-a'; my $default_rsync_long_args = '--delete --numeric-ids --relative --delete-excluded'; my $default_ssh_args = undef; my $default_du_args = '-csh'; # set default for use_lazy_deletes my $use_lazy_deletes = 0; # do not delete the oldest archive until after backup # set default for number of tries my $rsync_numtries = 1; # by default, try once # exactly how the program was called, with all arguments # this is set before getopts() modifies @ARGV my $run_string = "$0 " . join(' ', @ARGV); # if we have any errors, we print the run string once, at the top of the list my $have_printed_run_string = 0; # pre-buffer the include/exclude parameter flags # local to parse_config_file and validate_config_file my $rsync_include_args = undef; my $rsync_include_file_args = undef; # hash used to register traps and execute in bail my %traps; $traps{"linux_lvm_snapshot"} = 0; $traps{"linux_lvm_mountpoint"} = 0; ######################################## ### SIGNAL HANDLERS ### ######################################## # shut down gracefully if necessary $SIG{'HUP'} = 'IGNORE'; $SIG{'INT'} = sub { bail('rsnapshot was sent INT signal... cleaning up'); }; $SIG{'QUIT'} = sub { bail('rsnapshot was sent QUIT signal... cleaning up'); }; $SIG{'ABRT'} = sub { bail('rsnapshot was sent ABRT signal... cleaning up'); }; $SIG{'TERM'} = sub { bail('rsnapshot was sent TERM signal... cleaning up'); }; # For a PIPE error, we dont want any more output so set $verbose less than 1. $SIG{'PIPE'} = sub { $verbose = 0; bail( 'rsnapshot was sent PIPE signal... Hint: if rsnapshot is running from cron, check that mail is installed on this system, or redirect stdout and stderr in cron job' ); }; ######################################## ### CORE PROGRAM STRUCTURE ### ######################################## # what follows is a linear sequence of events. # all of these subroutines will either succeed or terminate the program safely. # figure out the path to the default config file (with autoconf we have to check) # this sets $config_file to the full config file path find_config_file(); # parse command line options # (this can override $config_file, if the -c flag is used on the command line) parse_cmd_line_opts(); # if we need to run a command that doesn't require fully parsing the config file, do it now (and exit) if (!defined($cmd) or ((!$cmd) && ('0' ne $cmd))) { show_usage(); } elsif ($cmd eq 'help') { show_help(); } elsif ($cmd eq 'version') { show_version(); } elsif ($cmd eq 'version-only') { show_version_only(); } elsif ($cmd eq 'check-config-version') { check_config_version(); } elsif ($cmd eq 'upgrade-config-file') { upgrade_config_file(); } # if we're just doing a configtest, set that flag if ($cmd eq 'configtest') { $do_configtest = 1; } # parse config file (if it exists), note: we can't parse a directory if (defined($config_file) && (-r $config_file) && (!-d $config_file)) { # if there is a problem, this subroutine will exit the program and notify the user of the error parse_config_file(); validate_config_file(); } # no config file found else { # warn user and exit the program exit_no_config_file(); } # attempt to load the Lchown module: http://search.cpan.org/dist/Lchown/ use_lchown(); # if we're just doing a configtest, exit here with the results if (1 == $do_configtest) { exit_configtest(); } # if we're just using "du" or "rsnapshot-diff" to check the disk space, do it now (and exit) # these commands are down here because they needs to know the contents of the config file if ($cmd eq 'du') { show_disk_usage(); } elsif ($cmd eq 'diff') { show_rsnapshot_diff(); } elsif ($cmd eq 'get-latest-snapshot') { show_latest_snapshot(); } # # IF WE GOT THIS FAR, PREPARE TO RUN A BACKUP # # log the beginning of this run log_startup(); # this is reported to fix some semi-obscure problems with rmtree() set_posix_locale(); # if we're using a lockfile, try to add it # (the program will bail if one exists and it's not stale) add_lockfile(); # create snapshot_root if it doesn't exist (and no_create_root != 1) create_snapshot_root(); # now chdir to the snapshot_root. # note that this is needed because in the rare case that you do this ... # sudo -u peon rsnapshot ... and are in a directory that 'peon' can't # read, then some versions of GNU rm will later fail, as they try to # lstat the cwd. It's safe to chdir because all directories etc that # we ever mention are absolute. chdir($config_vars{'snapshot_root'}); # actually run the backup job # $cmd should store the name of the interval we'll run against handle_interval($cmd); # if we have a lockfile, remove it # however, this will have already been done if use_lazy_deletes is turned # on, and there may be a lockfile from another process now in place, # so in that case don't just blindly delete! remove_lockfile() unless ($use_lazy_deletes); # if we got this far, the program is done running # write to the log and syslog with the status of the outcome # exit_with_status(); ######################################## ### SUBROUTINES ### ######################################## # concise usage information # runs when rsnapshot is called with no arguments # exits with an error condition sub show_usage { print < 1) { for (my $i = 1; $i < scalar(@ARGV); $i++) { print STDERR "Unknown option: $ARGV[$i]\n"; print STDERR "Please make sure all switches come before commands\n"; print STDERR "(e.g., 'rsnapshot -v alpha', not 'rsnapshot alpha -v')\n"; exit(1); } $result = undef; } } # alternate config file? if (defined($opts{'c'})) { $config_file = $opts{'c'}; } # test? (just show what WOULD be done) if (defined($opts{'t'})) { $test = 1; $verbose = 3; } # quiet? if (defined($opts{'q'})) { $verbose = 1; } # verbose (or extra verbose)? if (defined($opts{'v'})) { $verbose = 3; } if (defined($opts{'V'})) { $verbose = 4; } # debug if (defined($opts{'D'})) { $verbose = 5; } # one file system? (don't span partitions with rsync) if (defined($opts{'x'})) { $one_fs = 1; } } # accepts an optional argument - no arg means to parse the default file, # if an arg is present parse that file instead # returns no value # this subroutine parses the config file (rsnapshot.conf) # sub parse_config_file { # count the lines in the config file, so the user can pinpoint errors more precisely my $file_line_num = 0; my @configs = (); # open the config file my $current_config_file = shift() || $config_file; my $CONFIG; if ($current_config_file =~ /^`(.*)`$/) { open($CONFIG, "$1 |") or bail("Couldn't execute \"$1\" to get config information\n"); } else { $CONFIG = IO::File->new($current_config_file) or bail("Could not open config file \"$current_config_file\"\nAre you sure you have permission?"); } # read it line by line @configs = <$CONFIG>; while (my $line = $configs[$file_line_num]) { chomp($line); # count line numbers $file_line_num++; # Ensure the correct filename is reported in error messages. Setting it on # every iteration ensures it will be reset after recursive calls to this # function. $config_file = $current_config_file; # assume the line is formatted incorrectly my $line_syntax_ok = 0; # ignore comments if (is_comment($line)) { next; } # ignore blank lines if (is_blank($line)) { next; } # if the next line begins with space or tab and also has a non-space character, then it belongs to this line as a continuation. while (defined($configs[$file_line_num]) && $configs[$file_line_num] =~ /^[\t ]+\S/) { (my $newline = $configs[$file_line_num]) =~ s/^\s+|\s+$//g; $line = $line . "\t" . $newline; $file_line_num++; } # parse line my ($var, $value, $value2, $value3) = split(/\t+/, $line, 4); # warn about entries we don't understand, and immediately prevent the # program from running or parsing anything else if (!defined($var)) { config_err($file_line_num, "$line - could not find a first word on this line"); next; } if (!defined($value) && $var eq $line) { # No tabs found in $line. if ($line =~ /\s/) { # User put spaces in config line instead of tabs. config_err($file_line_num, "$line - missing tabs to separate words - change spaces to tabs."); next; } else { # User put only one word config_err($file_line_num, "$line - could not find second word on this line"); next; } } foreach (grep { defined($_) && index($_, ' ') == 0 } ($value, $value2, $value3)) { print_warn("$line - extra space found between tab and $_"); } # INCLUDEs if ($var eq 'include_conf') { $value =~ /^`(.*)`$/; if ( (defined($value) && -f $value && -r $value) || (defined($1) && is_valid_script($1))) { $line_syntax_ok = 1; parse_config_file($value); } else { if (defined($1)) { config_err($file_line_num, "$line - not a valid script: '$1'"); } else { config_err($file_line_num, "$line - can't find or read file '$value'"); } next; } } # CONFIG_VERSION if ($var eq 'config_version') { if (defined($value)) { # right now 1.2 is the only possible value if ('1.2' eq $value) { $config_vars{'config_version'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - config_version not recognized!"); next; } } else { config_err($file_line_num, "$line - config_version not defined!"); next; } } # SNAPSHOT_ROOT if ($var eq 'snapshot_root') { # make sure this is a full path if (0 == is_valid_local_abs_path($value)) { if (is_ssh_path($value) || is_anon_rsync_path($value) || is_cwrsync_path($value)) { config_err($file_line_num, "$line - snapshot_root must be a local path - you cannot have a remote snapshot_root"); } else { config_err($file_line_num, "$line - snapshot_root must be a full path"); } next; # if the snapshot root already exists: } elsif (-e "$value") { # if path exists already, make sure it's a directory if ((-e "$value") && (!-d "$value")) { config_err($file_line_num, "$line - snapshot_root must be a directory"); next; } # make sure it's readable if (!-r "$value") { config_err($file_line_num, "$line - snapshot_root exists but is not readable"); next; } # make sure it's writable if ($cmd ne 'du' && !-w "$value") { config_err($file_line_num, "$line - snapshot_root exists but is not writable"); next; } } # remove the trailing slash(es) if present $value = remove_trailing_slash($value); $config_vars{'snapshot_root'} = $value; $line_syntax_ok = 1; next; } # SYNC_FIRST # if this is enabled, rsnapshot syncs data to a staging directory with the "rsnapshot sync" command, # and all "interval" runs will simply rotate files. this changes the behaviour of the lowest interval. # when a sync occurs, no directories are rotated. the sync directory is kind of like a staging area for data transfers. # the files in the sync directory will be hard linked with the others in the other snapshot directories. # the sync directory lives at: //.sync/ # if ($var eq 'sync_first') { if (defined($value)) { if ('1' eq $value) { $config_vars{'sync_first'} = 1; $line_syntax_ok = 1; next; } elsif ('0' eq $value) { $config_vars{'sync_first'} = 0; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - sync_first must be set to either 1 or 0"); next; } } } # NO_CREATE_ROOT if ($var eq 'no_create_root') { if (defined($value)) { if ('1' eq $value) { $config_vars{'no_create_root'} = 1; $line_syntax_ok = 1; next; } elsif ('0' eq $value) { $config_vars{'no_create_root'} = 0; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - no_create_root must be set to either 1 or 0"); next; } } } # CHECK FOR RSYNC (required) if ($var eq 'cmd_rsync') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_rsync'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # CHECK FOR SSH (optional) if ($var eq 'cmd_ssh') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_ssh'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # CHECK FOR GNU cp (optional) if ($var eq 'cmd_cp') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_cp'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # CHECK FOR rm (optional) if ($var eq 'cmd_rm') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_rm'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # CHECK FOR LOGGER (syslog program) (optional) if ($var eq 'cmd_logger') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_logger'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # CHECK FOR du (optional) if ($var eq 'cmd_du') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_du'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # CHECK FOR lvcreate (optional) if ($var eq 'linux_lvm_cmd_lvcreate') { if (is_valid_script($value)) { $config_vars{'linux_lvm_cmd_lvcreate'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not a valid executable"); next; } } # CHECK FOR lvremove (optional) if ($var eq 'linux_lvm_cmd_lvremove') { if (is_valid_script($value)) { $config_vars{'linux_lvm_cmd_lvremove'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not a valid executable"); next; } } # CHECK FOR mount (optional) if ($var eq 'linux_lvm_cmd_mount') { if (is_valid_script($value)) { $config_vars{'linux_lvm_cmd_mount'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not a valid executable"); next; } } # CHECK FOR umount (optional) if ($var eq 'linux_lvm_cmd_umount') { if (is_valid_script($value)) { $config_vars{'linux_lvm_cmd_umount'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not a valid executable"); next; } } # CHECK FOR cmd_preexec (optional) if ($var eq 'cmd_preexec') { my $script; # script file (no args) # make sure script exists and is executable if (!is_valid_script($value, \$script)) { config_err($file_line_num, "$line - \"$script\" is not executable or can't be found." . ($script !~ m{^/} ? " Please use an absolute path." : "")); next; } $config_vars{$var} = $value; $line_syntax_ok = 1; next; } # CHECK FOR cmd_postexec (optional) if ($var eq 'cmd_postexec') { my $script; # script file (no args) # make sure script exists and is executable if (!is_valid_script($value, \$script)) { config_err($file_line_num, "$line - \"$script\" is not executable or can't be found." . ($script !~ m{^/} ? " Please use an absolute path." : "")); next; } $config_vars{$var} = $value; $line_syntax_ok = 1; next; } # CHECK FOR rsnapshot-diff (optional) if ($var eq 'cmd_rsnapshot_diff') { $value =~ s/\s+$//; if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) { $config_vars{'cmd_rsnapshot_diff'} = $value; $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - $value is not executable"); next; } } # INTERVALS # 'retain' is the new name for this parameter, although for # Laziness reasons (plus the fact that I'm making this change # at 10 minutes to midnight and so am wary of making changes # throughout the code and getting it wrong) the code will # still call it 'interval'. Documentation and messages should # refer to 'retain'. The old 'interval' will be kept as an # alias. if ($var eq 'interval' || $var eq 'retain') { my $retain = $var; # either 'interval' or 'retain' # check if interval is blank if (!defined($value)) { config_err($file_line_num, "$line - $retain can not be blank"); } foreach my $word (@reserved_words) { if ($value eq $word) { config_err($file_line_num, "$line - \"$value\" is not a valid interval name, reserved word conflict"); next; } } # make sure interval is alpha-numeric if ($value !~ m/^[\w\d]+$/) { config_err($file_line_num, "$line - \"$value\" is not a valid $retain name, must be alphanumeric characters only"); next; } # check if number is blank if (!defined($value2)) { config_err($file_line_num, "$line - \"$value\" number can not be blank"); next; } # check if number is valid if ($value2 !~ m/^\d+$/) { config_err($file_line_num, "$line - \"$value2\" is not a legal value for a retention count"); next; } # ok, it's a number. is it positive? else { # make sure number is positive if ($value2 <= 0) { config_err($file_line_num, "$line - \"$value\" must be at least 1 or higher"); next; } } my %hash; $hash{'interval'} = $value; $hash{'number'} = $value2; push(@intervals, \%hash); $line_syntax_ok = 1; next; } # BACKUP POINTS if ($var eq 'backup') { my $src = $value; # source directory my $dest = $value2; # dest directory my $opt_str = $value3; # option string from this backup point my $opts_ref = undef; # array_ref to hold parsed opts if (!defined($config_vars{'snapshot_root'})) { config_err($file_line_num, "$line - snapshot_root needs to be defined before backup points"); next; } if (!defined($src)) { config_err($file_line_num, "$line - no source path specified for backup point"); next; } if (!defined($dest) || $dest eq "") { config_err($file_line_num, "$line - no destination path specified for backup point"); next; } # make sure we aren't traversing directories if (is_directory_traversal($src)) { config_err($file_line_num, "$line - Directory traversal attempted in $src"); next; } if (is_directory_traversal($dest)) { config_err($file_line_num, "$line - Directory traversal attempted in $dest"); next; } # validate source path # # local absolute? if (is_real_local_abs_path($src)) { $line_syntax_ok = 1; # syntactically valid remote ssh? } elsif (is_ssh_path($src)) { # if it's an ssh path, make sure we have ssh if (!defined($config_vars{'cmd_ssh'})) { config_err($file_line_num, "$line - Cannot handle $src, cmd_ssh not defined in $config_file"); next; } $line_syntax_ok = 1; # if it's anonymous rsync, we're ok } elsif (is_anon_rsync_path($src)) { $line_syntax_ok = 1; # check for cwrsync } elsif (is_cwrsync_path($src)) { $line_syntax_ok = 1; # check for lvm } elsif (is_linux_lvm_path($src)) { # if it's an lvm path, make sure we have lvm commands and arguments if (!defined($config_vars{'linux_lvm_cmd_lvcreate'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_cmd_lvcreate not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_cmd_lvremove'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_cmd_lvremove not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_cmd_mount'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_cmd_mount not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_cmd_umount'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_cmd_umount not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_snapshotsize'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_snapshotsize not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_snapshotname'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_snapshotname not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_vgpath'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_vgpath not defined in $config_file"); next; } if (!defined($config_vars{'linux_lvm_mountpath'})) { config_err($file_line_num, "$line - Cannot handle $src, linux_lvm_mountpath not defined in $config_file"); next; } $line_syntax_ok = 1; } # fear the unknown else { config_err($file_line_num, "$line - Source directory \"$src\" doesn't exist"); next; } # validate destination path # # make sure we have a local NON absolute path for dest if (!is_valid_local_non_abs_path($dest)) { config_err($file_line_num, "$line - Backup destination $dest must be a local, relative path"); next; } # if we have special options specified for this backup point, remember them if (defined($opt_str) && $opt_str) { $opts_ref = parse_backup_opts($opt_str); if (!defined($opts_ref)) { config_err($file_line_num, "$line - Syntax error on line $file_line_num in extra opts: $opt_str"); next; } } # remember src/dest my %hash; $hash{'src'} = $src; $hash{'dest'} = normalize_dest_file_path_part($dest); if (defined($opts_ref)) { $hash{'opts'} = $opts_ref; } # If this backup point contains the snapshot root, add an exclude to avoid # backing up the snapshot root recursively. The exclude is anchored (by virtue # of the leading slash of $config_vars{'snapshot_root'}) and applies to absolute # paths (the "/" modifier), so it should match the snapshot root and nothing else # regardless of --relative. # # This should work in any version of rsync since 2.6.4 except for 2.6.7, due to a bug: # http://lists.samba.org/archive/rsync/2006-March/014953.html if ((is_real_local_abs_path("$src")) && ($config_vars{'snapshot_root'} =~ m/^$src/)) { $hash{'opts'}{'extra_rsync_long_args'} .= sprintf(' --filter=-/_%s', $config_vars{'snapshot_root'}); } push(@backup_points, \%hash); next; } # BACKUP SCRIPTS if ($var eq 'backup_script') { my $full_script = $value; # backup script to run (including args) my $dest = $value2; # dest directory my %hash; # tmp hash to stick in the backup points array my $script; # script file (no args) my @script_argv; # tmp array to help us separate the script from the args if (!defined($config_vars{'snapshot_root'})) { config_err($file_line_num, "$line - snapshot_root needs to be defined before backup scripts"); next; } if (!defined($dest)) { config_err($file_line_num, "$line - no destination path specified"); next; } # get the base name of the script, not counting any arguments to it @script_argv = split(/\s+/, $full_script); $script = $script_argv[0]; # make sure the destination is a relative path if (0 == is_valid_local_non_abs_path($dest)) { config_err($file_line_num, "$line - Backup destination $dest must be a local, relative path"); next; } # make sure we aren't traversing directories (exactly 2 dots can't be next to each other) if (1 == is_directory_traversal($dest)) { config_err($file_line_num, "$line - Directory traversal attempted in $dest"); next; } # make sure script exists and is executable if (((!-f "$script") or (!-x "$script")) or !is_real_local_abs_path($script)) { config_err($file_line_num, "$line - \"$script\" is not executable or can't be found." . ($script !~ m{^/} ? " Please use an absolute path." : "")); next; } $hash{'script'} = $full_script; $hash{'dest'} = normalize_dest_file_path_part($dest); $line_syntax_ok = 1; push(@backup_points, \%hash); next; } # BACKUP EXEC - just run a unix command if ($var eq 'backup_exec') { my %hash; my $cmd = $value; my $importance = $value2; if (!defined($cmd)) { config_err($file_line_num, "$line - a command to be executed must be provided"); next; } # Valid importance level options: 'optional', 'required'. # Default value if not specified: 'optional' if (!defined($importance)) { $importance = 'optional'; } elsif ($importance ne 'optional' && $importance ne 'required') { config_err($file_line_num, "$line - requirement level \"$importance\" is invalid"); next; } $hash{'cmd'} = $cmd; $hash{'importance'} = $importance; $line_syntax_ok = 1; push(@backup_points, \%hash); next; } # GLOBAL OPTIONS from the config file # ALL ARE OPTIONAL # # LINK_DEST if ($var eq 'link_dest') { if (!defined($value)) { config_err($file_line_num, "$line - link_dest can not be blank"); next; } if (!is_boolean($value)) { config_err($file_line_num, "$line - \"$value\" is not a legal value for link_dest, must be 0 or 1 only"); next; } $link_dest = $value; $line_syntax_ok = 1; next; } # ONE_FS if ($var eq 'one_fs') { if (!defined($value)) { config_err($file_line_num, "$line - one_fs can not be blank"); next; } if (!is_boolean($value)) { config_err($file_line_num, "$line - \"$value\" is not a legal value for one_fs, must be 0 or 1 only"); next; } $one_fs = $value; $line_syntax_ok = 1; next; } # LOCKFILE if ($var eq 'lockfile') { if (!defined($value)) { config_err($file_line_num, "$line - lockfile can not be blank"); } if (0 == is_valid_local_abs_path("$value")) { config_err($file_line_num, "$line - lockfile must be a full path"); next; } $config_vars{'lockfile'} = $value; $line_syntax_ok = 1; next; } #STOP_ON_STALE_LOCKFILE if ($var eq 'stop_on_stale_lockfile') { if (!defined($value)) { config_err($file_line_num, "$line - stop_on_stale_lockfile can not be blank"); next; } if (!is_boolean($value)) { config_err($file_line_num, "$line - \"$value\" is not a legal value for stop_on_stale_lockfile, must be 0 or 1 only"); next; } $stop_on_stale_lockfile = $value; $line_syntax_ok = 1; next; } # INCLUDE if ($var eq 'include') { if (!defined($rsync_include_args)) { $rsync_include_args = "--include=$value"; } else { $rsync_include_args .= " --include=$value"; } $line_syntax_ok = 1; next; } # EXCLUDE if ($var eq 'exclude') { if (!defined($rsync_include_args)) { $rsync_include_args = "--exclude=$value"; } else { $rsync_include_args .= " --exclude=$value"; } $line_syntax_ok = 1; next; } # INCLUDE FILE if ($var eq 'include_file') { if (0 == is_real_local_abs_path($value)) { config_err($file_line_num, "$line - include_file $value must be a valid absolute path"); next; } elsif (1 == is_directory_traversal($value)) { config_err($file_line_num, "$line - Directory traversal attempted in $value"); next; } elsif ((-e "$value") && (!-f "$value")) { config_err($file_line_num, "$line - include_file $value exists, but is not a file"); next; } elsif (!-r "$value") { config_err($file_line_num, "$line - include_file $value exists, but is not readable"); next; } else { if (!defined($rsync_include_file_args)) { $rsync_include_file_args = "--include-from=$value"; } else { $rsync_include_file_args .= " --include-from=$value"; } $line_syntax_ok = 1; next; } } # EXCLUDE FILE if ($var eq 'exclude_file') { if (0 == is_real_local_abs_path($value)) { config_err($file_line_num, "$line - exclude_file $value must be a valid absolute path"); next; } elsif (1 == is_directory_traversal($value)) { config_err($file_line_num, "$line - Directory traversal attempted in $value"); next; } elsif ((-e "$value") && (!-f "$value")) { config_err($file_line_num, "$line - exclude_file $value exists, but is not a file"); next; } elsif (!-r "$value") { config_err($file_line_num, "$line - exclude_file $value exists, but is not readable"); next; } else { if (!defined($rsync_include_file_args)) { $rsync_include_file_args = "--exclude-from=$value"; } else { $rsync_include_file_args .= " --exclude-from=$value"; } $line_syntax_ok = 1; next; } } # RSYNC SHORT ARGS if ($var eq 'rsync_short_args') { # must be in the format '-abcde' if (0 == is_valid_rsync_short_args($value)) { config_err($file_line_num, "$line - rsync_short_args \"$value\" not in correct format"); next; } else { $config_vars{'rsync_short_args'} = $value; $line_syntax_ok = 1; next; } } # RSYNC LONG ARGS if ($var eq 'rsync_long_args') { $config_vars{'rsync_long_args'} = $value; $line_syntax_ok = 1; next; } # SSH ARGS if ($var eq 'ssh_args') { if (!defined($default_ssh_args) && defined($config_vars{'ssh_args'})) { config_err($file_line_num, "$line - global ssh_args can only be set once, but is already set. Perhaps you wanted to use a per-backup-point ssh_args instead." ); next; } else { $config_vars{'ssh_args'} = $value; $line_syntax_ok = 1; next; } } # DU ARGS if ($var eq 'du_args') { $config_vars{'du_args'} = $value; $line_syntax_ok = 1; next; } # LVM CMDS if ($var =~ m/^linux_lvm_cmd_(lvcreate|mount)$/) { $config_vars{$var} = $value; $line_syntax_ok = 1; next; } # LVM ARGS if ($var =~ m/^linux_lvm_(vgpath|snapshotname|snapshotsize|mountpath)$/) { $config_vars{$var} = $value; $line_syntax_ok = 1; next; } # LOGFILE if ($var eq 'logfile') { if (0 == is_valid_local_abs_path($value)) { config_err($file_line_num, "$line - logfile must be a valid absolute path"); next; } elsif (1 == is_directory_traversal($value)) { config_err($file_line_num, "$line - Directory traversal attempted in $value"); next; } elsif ((-e "$value") && (!-f "$value") && (!-p "$value")) { config_err($file_line_num, "$line - logfile $value exists, but is not a file"); next; } else { $config_vars{'logfile'} = $value; $line_syntax_ok = 1; next; } } # VERBOSE if ($var eq 'verbose') { if (1 == is_valid_loglevel($value)) { if (!defined($verbose)) { $verbose = $value; } elsif($verbose < $value ) { print_warn("The verbosity-level is \"$verbose\" despite subsequent declaration at line $file_line_num."); } $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - verbose must be a value between 1 and 5"); next; } } # LOGLEVEL if ($var eq 'loglevel') { if (1 == is_valid_loglevel($value)) { if (!defined($loglevel)) { $loglevel = $value; } $line_syntax_ok = 1; next; } else { config_err($file_line_num, "$line - loglevel must be a value between 1 and 5"); next; } } # USE LAZY DELETES if ($var eq 'use_lazy_deletes') { if (!defined($value)) { config_err($file_line_num, "$line - use_lazy_deletes can not be blank"); next; } if (!is_boolean($value)) { config_err($file_line_num, "$line - \"$value\" is not a legal value for use_lazy_deletes, must be 0 or 1 only"); next; } if (1 == $value) { $use_lazy_deletes = 1; } $line_syntax_ok = 1; next; } # RSYNC NUMBER OF TRIES if ($var eq 'rsync_numtries') { if (!defined($value)) { config_err($file_line_num, "$line - rsync_numtries can not be blank"); next; } if (!is_valid_rsync_numtries($value)) { config_err($file_line_num, "$line - \"$value\" is not a legal value for rsync_numtries, must be greater than or equal to 0"); next; } $rsync_numtries = int($value); $line_syntax_ok = 1; next; } # make sure we understood this line # if not, warn the user, and prevent the program from executing # however, don't bother if the user has already been notified if (1 == $config_perfect) { if (0 == $line_syntax_ok) { config_err($file_line_num, $line); next; } } } } sub validate_config_file { #################################################################### # SET SOME SENSIBLE DEFAULTS FOR VALUES THAT MAY NOT HAVE BEEN SET # #################################################################### # if we didn't manage to get a verbose level yet, either through the config file # or the command line, use the default if (!defined($verbose)) { $verbose = $default_verbose; } # same for loglevel if (!defined($loglevel)) { $loglevel = $default_loglevel; } # assemble rsync include/exclude args if (defined($rsync_include_args)) { if (!defined($config_vars{'rsync_long_args'})) { $config_vars{'rsync_long_args'} = $default_rsync_long_args; } $config_vars{'rsync_long_args'} .= " $rsync_include_args"; } # assemble rsync include/exclude file args if (defined($rsync_include_file_args)) { if (!defined($config_vars{'rsync_long_args'})) { $config_vars{'rsync_long_args'} = $default_rsync_long_args; } $config_vars{'rsync_long_args'} .= " $rsync_include_file_args"; } ############################################### # NOW THAT THE CONFIG FILE HAS BEEN READ IN, # # DO A SANITY CHECK ON THE DATA WE PULLED OUT # ############################################### # SINS OF COMMISSION # (incorrect entries in config file) if (0 == $config_perfect) { print_err("---------------------------------------------------------------------", 1); print_err("Errors were found in $config_file,", 1); print_err("rsnapshot can not continue. If you think an entry looks right, make", 1); print_err("sure you don't have spaces where only tabs should be.", 1); # if this wasn't a test, report the error to syslog if (0 == $do_configtest) { syslog_err("Errors were found in $config_file, rsnapshot can not continue."); } # exit showing an error exit(1); } # SINS OF OMISSION # (things that should be in the config file that aren't) # # make sure config_version was set if (!defined($config_vars{'config_version'})) { print_err("config_version was not defined. rsnapshot can not continue.", 1); syslog_err("config_version was not defined. rsnapshot can not continue."); exit(1); } # make sure rsync was defined if (!defined($config_vars{'cmd_rsync'})) { print_err("cmd_rsync was not defined.", 1); syslog_err("cmd_rsync was not defined.", 1); exit(1); } # make sure we got a snapshot_root if (!defined($config_vars{'snapshot_root'})) { print_err("snapshot_root was not defined. rsnapshot can not continue.", 1); syslog_err("snapshot_root was not defined. rsnapshot can not continue."); exit(1); } # make sure we have at least one interval if (0 == scalar(@intervals)) { print_err("At least one backup level must be set. rsnapshot can not continue.", 1); syslog_err("At least one backup level must be set. rsnapshot can not continue."); exit(1); } # make sure we have at least one backup point if (0 == scalar(@backup_points)) { print_err("At least one backup point must be set. rsnapshot can not continue.", 1); syslog_err("At least one backup point must be set. rsnapshot can not continue."); exit(1); } # SINS OF CONFUSION # (various, specific, undesirable interactions) # # make sure that we don't have only one copy of the first interval, # yet expect rotations on the second interval if (scalar(@intervals) > 1) { if (defined($intervals[0]->{'number'})) { if (1 == $intervals[0]->{'number'}) { print_err( "Can not have first backup level's retention count set to 1, and have a second backup level", 1); syslog_err( "Can not have first backup level's retention count set to 1, and have a second backup level"); exit(1); } } } # make sure that the snapshot_root exists if no_create_root is set to 1 if (defined($config_vars{'no_create_root'})) { if (1 == $config_vars{'no_create_root'}) { if (!-d "$config_vars{'snapshot_root'}") { if (-e "$config_vars{'snapshot_root'}") { print_err("$config_vars{'snapshot_root'} is not a directory.", 1); } else { my $snapshot_root = $config_vars{'snapshot_root'}; # Check parent directories until we find one that exists while (!-e $snapshot_root) { print_err("$snapshot_root does not exist.", 1); $snapshot_root =~ m%(.*)/[^/]*%; if (defined($1) && $1 ne $snapshot_root) { $snapshot_root = $1; } else { last; } } if (-e $snapshot_root && !-d $snapshot_root) { print_err("$snapshot_root is not a directory.", 1); syslog_err("$snapshot_root is not a directory."); } } print_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled", 1); syslog_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled"); exit(1); } } } # make sure that the user didn't call "sync" if sync_first isn't enabled if (($cmd eq 'sync') && (!$config_vars{'sync_first'})) { print_err("\"sync_first\" must be enabled for \"sync\" to work", 1); syslog_err("\"sync_first\" must be enabled for \"sync\" to work"); exit(1); } # make sure that the backup_script destination paths don't nuke data copied over for backup points { my @backup_dest = (); my @backup_script_dest = (); # remember where the destination paths are... foreach my $bp_ref (@backup_points) { # skip for backup_exec since it uses no destination next if (defined($$bp_ref{'cmd'})); # backup if (defined($$bp_ref{'src'})) { push(@backup_dest, $$bp_ref{'dest'}); # backup_script } elsif (defined($$bp_ref{'script'})) { push(@backup_script_dest, $$bp_ref{'dest'}); } # something else is wrong else { print_err("logic error in parse_config_file(): a backup point has no src and no script", 1); syslog_err("logic error in parse_config_file(): a backup point has no src and no script"); exit(1); } } # loop through and check for conflicts between backup and backup_script destination paths foreach my $b_dest (@backup_dest) { foreach my $bs_dest (@backup_script_dest) { if (defined($b_dest) && defined($bs_dest)) { my $tmp_b = $b_dest; my $tmp_bs = $bs_dest; # add trailing slashes back in so similarly named directories don't match # e.g., localhost/abc/ and localhost/ab/ $tmp_b .= '/'; $tmp_bs .= '/'; if ("$tmp_b" =~ m/^$tmp_bs/) { # duplicate entries, stop here print_err( "destination conflict between \"$tmp_b\" and \"$tmp_bs\" in backup / backup_script entries", 1); syslog_err( "destination conflict between \"$tmp_b\" and \"$tmp_bs\" in backup / backup_script entries"); exit(1); } } else { print_err("logic error in parse_config_file(): unique destination check failed unexpectedly", 1); syslog_err("logic error in parse_config_file(): unique destination check failed unexpectedly"); exit(1); } } } # loop through and check for conflicts between different backup_scripts for (my $i = 0; $i < scalar(@backup_script_dest); $i++) { for (my $j = 0; $j < scalar(@backup_script_dest); $j++) { if ($i != $j) { my $path1 = $backup_script_dest[$i]; my $path2 = $backup_script_dest[$j]; # add trailing slashes back in so similarly named directories don't match # e.g., localhost/abc/ and localhost/ab/ $path1 .= '/'; $path2 .= '/'; if (("$path1" =~ m/^$path2/) or ("$path2" =~ m/^$path1/)) { print_err( "destination conflict between \"$path1\" and \"$path2\" in multiple backup_script entries", 1); syslog_err( "destination conflict between \"$path1\" and \"$path2\" in multiple backup_script entries"); exit(1); } } } } } } # accepts a string of options # returns an array_ref of parsed options # returns undef if there is an invalid option # # this is for individual backup points only sub parse_backup_opts { my $opts_str = shift(@_); my @pairs; my %parsed_opts; # pre-buffer extra rsync arguments my $rsync_include_args = undef; my $rsync_include_file_args = undef; # make sure we got something (it's quite likely that we didn't) if (!defined($opts_str)) { return (undef); } if (!$opts_str) { return (undef); } # split on commas first @pairs = split(/,/, $opts_str); # then loop through and split on equals foreach my $pair (@pairs) { my $additive; if ($pair =~ /^\+/) { $additive = 1; $pair =~ s/^.//; } else { $additive = 0; } my ($name, $value) = split(/=/, $pair, 2); if (!defined($name) or !defined($value)) { return (undef); } # parameters can't have spaces in them $name =~ s/\s+//go; # strip whitespace from both ends $value =~ s/^\s{0,}//o; $value =~ s/\s{0,}$//o; # ok, it's a name/value pair and it's ready for more validation if ($additive) { $parsed_opts{'extra_' . $name} = $value; } else { $parsed_opts{$name} = $value; } # VALIDATE ARGS # one_fs if ($name eq 'one_fs') { if (!is_boolean($parsed_opts{'one_fs'})) { return (undef); } # rsync_short_args } elsif ($name eq 'rsync_short_args') { # must be in the format '-abcde' if (0 == is_valid_rsync_short_args($value)) { print_err("rsync_short_args \"$value\" not in correct format", 2); return (undef); } # rsync_long_args } elsif ($name eq 'rsync_long_args') { # pass unchecked # ssh_args } elsif ($name eq 'ssh_args') { # pass unchecked # lvm args } elsif ($name =~ m/^linux_lvm_(vgpath|snapshotname|snapshotsize|mountpath)$/) { # pass unchecked # include } elsif ($name eq 'include') { # don't validate contents # coerce into rsync_include_args # then remove the "include" key/value pair if (!defined($rsync_include_args)) { $rsync_include_args = "--include=$parsed_opts{'include'}"; } else { $rsync_include_args .= " --include=$parsed_opts{'include'}"; } delete($parsed_opts{'include'}); # exclude } elsif ($name eq 'exclude') { # don't validate contents # coerce into rsync_include_args # then remove the "include" key/value pair if (!defined($rsync_include_args)) { $rsync_include_args = "--exclude=$parsed_opts{'exclude'}"; } else { $rsync_include_args .= " --exclude=$parsed_opts{'exclude'}"; } delete($parsed_opts{'exclude'}); # include_file } elsif ($name eq 'include_file') { # verify that this file exists and is readable if (0 == is_real_local_abs_path($value)) { print_err("include_file $value must be a valid absolute path", 2); return (undef); } elsif (1 == is_directory_traversal($value)) { print_err("Directory traversal attempted in $value", 2); return (undef); } elsif ((-e "$value") && (!-f "$value")) { print_err("include_file $value exists, but is not a file", 2); return (undef); } elsif (!-r "$value") { print_err("include_file $value exists, but is not readable", 2); return (undef); } # coerce into rsync_include_file_args # then remove the "include_file" key/value pair if (!defined($rsync_include_file_args)) { $rsync_include_file_args = "--include-from=$parsed_opts{'include_file'}"; } else { $rsync_include_file_args .= " --include-from=$parsed_opts{'include_file'}"; } delete($parsed_opts{'include_file'}); # exclude_file } elsif ($name eq 'exclude_file') { # verify that this file exists and is readable if (0 == is_real_local_abs_path($value)) { print_err("exclude_file $value must be a valid absolute path", 2); return (undef); } elsif (1 == is_directory_traversal($value)) { print_err("Directory traversal attempted in $value", 2); return (undef); } elsif ((-e "$value") && (!-f "$value")) { print_err("exclude_file $value exists, but is not a file", 2); return (undef); } elsif (!-r "$value") { print_err("exclude_file $value exists, but is not readable", 2); return (undef); } # coerce into rsync_include_file_args # then remove the "exclude_file" key/value pair if (!defined($rsync_include_file_args)) { $rsync_include_file_args = "--exclude-from=$parsed_opts{'exclude_file'}"; } else { $rsync_include_file_args .= " --exclude-from=$parsed_opts{'exclude_file'}"; } delete($parsed_opts{'exclude_file'}); # Not (yet?) implemented as per-backup-point options } elsif ($name eq 'cmd_preexec' || $name eq 'cmd_postexec' || $name eq 'cmd_ssh' || $name eq 'cmd_rsync' || $name eq 'verbose' || $name eq 'loglevel') { print_err("$name is not implemented as a per-backup-point option in this version of rsnapshot", 2); return (undef); } # if we don't know about it, it doesn't exist else { return (undef); } } # merge rsync_include_args and rsync_file_include_args in with either $default_rsync_long_args # or $parsed_opts{'rsync_long_args'} if (defined($rsync_include_args) or defined($rsync_include_file_args)) { # if we never defined rsync_long_args, populate it with the global default if (!defined($parsed_opts{'rsync_long_args'})) { if (defined($config_vars{'rsync_long_args'})) { $parsed_opts{'rsync_long_args'} = $config_vars{'rsync_long_args'}; } else { $parsed_opts{'rsync_long_args'} = $default_rsync_long_args; } } # now we have something in our local rsync_long_args # let's concatenate the include/exclude/file stuff to it if (defined($rsync_include_args)) { $parsed_opts{'rsync_long_args'} .= " $rsync_include_args"; } if (defined($rsync_include_file_args)) { $parsed_opts{'rsync_long_args'} .= " $rsync_include_file_args"; } } # if we got anything, return it as an array_ref if (%parsed_opts) { return (\%parsed_opts); } return (undef); } # accepts line number, errstr # prints a config file error # also sets global $config_perfect var off sub config_err { my $line_num = shift(@_); my $errstr = shift(@_); if (!defined($line_num)) { $line_num = -1; } if (!defined($errstr)) { $errstr = 'config_err() called without an error string!'; } # show the user the file and line number print_err("$config_file on line $line_num:", 1); # print out the offending line # don't print past 69 columns (because they all start with 'ERROR: ') # similarly, indent subsequent lines 9 spaces to get past the 'ERROR: ' message print_err(wrap_cmd($errstr, 69, 9), 1); # invalidate entire config file $config_perfect = 0; } # accepts an error string # prints to STDERR and maybe syslog. removes the lockfile if it exists. # exits the program safely and consistently sub bail { my $str = shift(@_); # print out error if ($str) { print_err($str, 1); } # write to syslog if we're running for real (and we have a message) if ((0 == $do_configtest) && (0 == $test) && defined($str) && ('' ne $str)) { syslog_err($str); } # umount LVM Snapshot if it is mounted if (1 == $traps{"linux_lvm_mountpoint"}) { $traps{"linux_lvm_mountpoint"} = 0; linux_lvm_unmount(); } # destroy snapshot created by rsnapshot if (0 ne $traps{"linux_lvm_snapshot"}) { my $tmp = $traps{"linux_lvm_snapshot"}; $traps{"linux_lvm_snapshot"} = 0; linux_lvm_snapshot_del(linux_lvm_parseurl($tmp)); } # get rid of the lockfile, if it exists if (0 == $stop_on_stale_lockfile) { remove_lockfile(); } # exit showing an error exit(1); } # accepts a string (or an array) # prints the string, but separates it across multiple lines with backslashes if necessary # also logs the command, but on a single line sub print_cmd { # take all arguments and make them into one string my $str = join(' ', @_); if (!defined($str)) { return (undef); } # remove newline and consolidate spaces chomp($str); $str =~ s/\s+/ /g; # write to log (level 3 is where we start showing commands) log_msg($str, 3); if (!defined($verbose) or ($verbose >= 3)) { print wrap_cmd($str), "\n"; } } # accepts a string # wraps the text to fit in 80 columns with backslashes at the end of each wrapping line # returns the wrapped string sub wrap_cmd { my $str = shift(@_); my $colmax = shift(@_); my $indent = shift(@_); my @tokens; my $chars = 0; # character tally my $outstr = ''; # string to return # max chars before wrap (default to 80 column terminal) if (!defined($colmax)) { $colmax = 76; } # number of spaces to indent subsequent lines if (!defined($indent)) { $indent = 4; } # break up string into individual pieces @tokens = split(/\s+/, $str); # stop here if we don't have anything if (0 == scalar(@tokens)) { return (''); } # print the first token as a special exception, since we should never start out by line wrapping if (defined($tokens[0])) { $chars = (length($tokens[0]) + 1); $outstr .= $tokens[0]; # don't forget to put the space back in if (scalar(@tokens) > 1) { $outstr .= ' '; } } # loop through the rest of the tokens and print them out, wrapping when necessary for (my $i = 1; $i < scalar(@tokens); $i++) { # keep track of where we are (plus a space) $chars += (length($tokens[$i]) + 1); # wrap if we're at the edge if ($chars > $colmax) { $outstr .= "\\\n"; $outstr .= (' ' x $indent); # 4 spaces + string length $chars = $indent + length($tokens[$i]); } # print out this token $outstr .= $tokens[$i]; # print out a space unless this is the last one if ($i < scalar(@tokens)) { $outstr .= ' '; } } return ($outstr); } # accepts string, and level # prints string if level is as high as verbose # logs string if level is as high as loglevel sub print_msg { my $str = shift(@_); my $level = shift(@_); if (!defined($str)) { return (undef); } if (!defined($level)) { $level = 0; } chomp($str); # print to STDOUT if ((!defined($verbose)) or ($verbose >= $level)) { print $str, "\n"; } # write to log log_msg($str, $level); } # accepts string, and level # prints string if level is as high as verbose # logs string if level is as high as loglevel # also raises a warning for the exit code sub print_warn { my $str = shift(@_); my $level = shift(@_); if (!defined($str)) { return (undef); } if (!defined($level)) { $level = 0; } # we can no longer say the execution of the program has been error free raise_warning(); chomp($str); # print to STDERR if ((!defined($verbose)) or ($level <= $verbose)) { print STDERR 'WARNING: ', $str, "\n"; } # write to log log_warn($str, $level); } # accepts string, and level # prints string if level is as high as verbose # logs string if level is as high as loglevel # also raises an error for the exit code sub print_err { my $str = shift(@_); my $level = shift(@_); if (!defined($str)) { return (undef); } if (!defined($level)) { $level = 0; } # we can no longer say the execution of the program has been error free raise_error(); chomp($str); # print the run string once # this way we know where the message came from if it's in an e-mail # but we can still read messages at the console if (0 == $have_printed_run_string) { if ((!defined($verbose)) or ($level <= $verbose)) { print STDERR "----------------------------------------------------------------------------\n"; print STDERR "rsnapshot encountered an error! The program was invoked with these options:\n"; print STDERR wrap_cmd($run_string), "\n"; print STDERR "----------------------------------------------------------------------------\n"; } $have_printed_run_string = 1; } # print to STDERR if ((!defined($verbose)) or ($level <= $verbose)) { #print STDERR $run_string, ": ERROR: ", $str, "\n"; print STDERR "ERROR: ", $str, "\n"; } # write to log log_err($str, $level); } # accepts string, and level # logs string if level is as high as loglevel sub log_msg { my $str = shift(@_); my $level = shift(@_); my $result = undef; if (!defined($str)) { return (undef); } if (!defined($level)) { return (undef); } chomp($str); # if this is just noise, don't log it if (defined($loglevel) && ($level > $loglevel)) { return (undef); } # open logfile, write to it, close it back up # if we fail, don't use the usual print_* functions, since they just call this again if ((0 == $test) && (0 == $do_configtest)) { if (defined($config_vars{'logfile'})) { $result = open(LOG, ">> $config_vars{'logfile'}"); if (!defined($result)) { print STDERR "Could not open logfile $config_vars{'logfile'} for writing\n"; print STDERR "Do you have write permission for this file?\n"; exit(1); } print LOG '[', get_current_date(), '] ', $str, "\n"; $result = close(LOG); if (!defined($result)) { print STDERR "Could not close logfile $config_vars{'logfile'}\n"; } } } } # accepts string, and level # logs string if level is as high as loglevel # also raises a warning for the exit code sub log_warn { my $str = shift(@_); my $level = shift(@_); if (!defined($str)) { return (undef); } if (!defined($level)) { return (undef); } # this run is no longer perfect since we have an error raise_warning(); chomp($str); $str = 'WARNING: ' . $str; log_msg($str, $level); } # accepts string, and level # logs string if level is as high as loglevel # also raises an error for the exit code sub log_err { my $str = shift(@_); my $level = shift(@_); if (!defined($str)) { return (undef); } if (!defined($level)) { return (undef); } # this run is no longer perfect since we have an error raise_error(); chomp($str); $str = "$run_string: ERROR: " . $str; log_msg($str, $level); } # log messages to syslog # accepts message, facility, level # only message is required # return 1 on success, undef on failure sub syslog_msg { my $msg = shift(@_); my $facility = shift(@_); my $level = shift(@_); my $result = undef; if (!defined($msg)) { return (undef); } if (!defined($facility)) { $facility = 'user'; } if (!defined($level)) { $level = 'info'; } if (defined($config_vars{'cmd_logger'})) { # print out our call to syslog if (defined($verbose) && ($verbose >= 4)) { print_cmd("$config_vars{'cmd_logger'} -p $facility.$level -t rsnapshot[$$] $msg"); } # log to syslog if (0 == $test) { $result = system($config_vars{'cmd_logger'}, '-p', "$facility.$level", '-t', "rsnapshot[$$]", $msg); if (0 != $result) { print_warn("Could not log to syslog:", 2); print_warn("$config_vars{'cmd_logger'} -p $facility.$level -t rsnapshot[$$] $msg", 2); } } } return (1); } # log warnings to syslog # accepts warning message # returns 1 on success, undef on failure # also raises a warning for the exit code sub syslog_warn { my $msg = shift(@_); # this run is no longer perfect since we have an error raise_warning(); return syslog_msg("WARNING: $msg", 'user', 'err'); } # log errors to syslog # accepts error message # returns 1 on success, undef on failure # also raises an error for the exit code sub syslog_err { my $msg = shift(@_); # this run is no longer perfect since we have an error raise_error(); return syslog_msg("$run_string: ERROR: $msg", 'user', 'err'); } # sets exit code for at least a warning sub raise_warning { if ($exit_code != 1) { $exit_code = 2; } } # sets exit code for error sub raise_error { $exit_code = 1; } # accepts no arguments # returns the current date (for the logfile) # # there's probably a wonderful module that can do this all for me, # but unless it comes standard with perl 5.004 and later, i'd rather do it this way :) # sub get_current_date { # localtime() gives us an array with these elements: # 0 = seconds # 1 = minutes # 2 = hours # 3 = day of month # 4 = month + 1 # 5 = year + 1900 # example date format (ISO 8601) # 2012-04-24T22:30:13 (used to be 28/Feb/2004:23:45:59, like Apache) my @fields = localtime(time()); return sprintf( "%04i-%02i-%02iT%02i:%02i:%02i", $fields[5] + 1900, $fields[4] + 1, $fields[3], $fields[2], $fields[1], $fields[0] ); } # accepts no arguments # returns nothing # simply prints out a startup message to the logs and STDOUT sub log_startup { log_msg("$run_string: started", 2); } # accepts no arguments # returns undef if lockfile isn't defined in the config file, and 1 upon success # also, it can make the program exit with 1 as the return value if it can't create the lockfile # # we don't use bail() to exit on error, because that would remove the # lockfile that may exist from another invocation # # if a lockfile exists, we try to read it (and stop if we can't) to get a PID, # then see if that PID exists. If it does, we stop, otherwise we assume it's # a stale lock and remove it first. sub add_lockfile { # if we don't have a lockfile defined, just return undef if (!defined($config_vars{'lockfile'})) { return (undef); } my $lockfile = $config_vars{'lockfile'}; # valid? if (0 == is_valid_local_abs_path($lockfile)) { print_err("Lockfile $lockfile is not a valid file name", 1); syslog_err("Lockfile $lockfile is not a valid file name"); exit(1); } # does a lockfile already exist? if (1 == is_real_local_abs_path($lockfile)) { if (!open(LOCKFILE, $lockfile)) { print_err("Lockfile $lockfile exists and can't be read, can not continue!", 1); syslog_err("Lockfile $lockfile exists and can't be read, can not continue"); exit(1); } my $pid = || ""; chomp($pid); close(LOCKFILE); if ($pid =~ m/^[0-9]+$/ && kill(0, $pid)) { print_err("Lockfile $lockfile exists and so does its process, can not continue"); syslog_err("Lockfile $lockfile exists and so does its process, can not continue"); exit(1); } else { if (1 == $stop_on_stale_lockfile) { print_err("Stale lockfile $lockfile detected. You need to remove it manually to continue", 1); syslog_err("Stale lockfile $lockfile detected. Exiting."); exit(1); } else { print_warn("Removing stale lockfile $lockfile", 1); syslog_warn("Removing stale lockfile $lockfile"); remove_lockfile(); } } } # create the lockfile print_cmd("echo $$ > $lockfile"); if (0 == $test) { # sysopen() can do exclusive opens, whereas perl open() can not my $result = sysopen(LOCKFILE, $lockfile, O_WRONLY | O_EXCL | O_CREAT, 0644); if (!defined($result) || 0 == $result) { print_err("Could not write lockfile $lockfile: $!", 1); syslog_err("Could not write lockfile $lockfile"); exit(1); } # print PID to lockfile print LOCKFILE $$; $result = close(LOCKFILE); if (!defined($result) || 0 == $result) { print_warn("Could not close lockfile $lockfile: $!", 2); } } return (1); } # accepts no arguments # # returns undef if lockfile isn't defined in the config file # return 1 upon success or if there's no lockfile to remove # warn if the PID in the lockfile is not the same as the PID of this process # exit with a value of 1 if it can't read the lockfile # exit with a value of 1 if it can't remove the lockfile # # we don't use bail() to exit on error, because that would call # this subroutine twice in the event of a failure sub remove_lockfile { # if we don't have a lockfile defined, return undef if (!defined($config_vars{'lockfile'})) { return (undef); } my $lockfile = $config_vars{'lockfile'}; my $result = undef; if (-e "$lockfile") { if (open(LOCKFILE, $lockfile)) { my $locked_pid = || ""; chomp($locked_pid); close(LOCKFILE); if ($locked_pid && $locked_pid != $$) { print_warn( "About to remove lockfile $lockfile which belongs to a different process: $locked_pid (this is OK if it's a stale lock)" ); } } else { print_err("Could not read lockfile $lockfile: $!", 0); syslog_err("Error! Could not read lockfile $lockfile: $!"); exit(1); } print_cmd("rm -f $lockfile"); if (0 == $test) { $result = unlink($lockfile); if (0 == $result) { print_err("Could not remove lockfile $lockfile", 1); syslog_err("Error! Could not remove lockfile $lockfile"); exit(1); } } } else { print_msg("No need to remove non-existent lock $lockfile", 5); } return (1); } # accepts no arguments # returns nothing # sets the locale to POSIX (C) to mitigate some problems with the rmtree() command # sub set_posix_locale { # set POSIX locale # this may fix some potential problems with rmtree() # another solution is to enable "cmd_rm" in rsnapshot.conf print_msg("Setting locale to POSIX \"C\"", 4); setlocale(POSIX::LC_ALL, 'C'); } # accepts no arguments # returns nothing # creates the snapshot_root directory (chmod 0700), if it doesn't exist and no_create_root == 0 sub create_snapshot_root { # attempt to create the directory if it doesn't exist if (!-d "$config_vars{'snapshot_root'}") { # make sure no_create_root == 0 if (defined($config_vars{'no_create_root'})) { if (1 == $config_vars{'no_create_root'}) { print_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled", 1); syslog_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled"); bail(); } } # actually create the directory print_cmd("mkdir -m 0700 -p $config_vars{'snapshot_root'}/"); if (0 == $test) { eval { # don't pass a trailing slash to mkpath mkpath("$config_vars{'snapshot_root'}", 0, 0700); }; if ($@) { bail( "Unable to create $config_vars{'snapshot_root'}/,\nPlease make sure you have the right permissions." ); } } } } # accepts current interval # returns a hash_ref containing information about the intervals # exits the program if we don't have good data to work with sub get_interval_data { my $cur_interval = shift(@_); # make sure we were passed an interval if (!defined($cur_interval)) { bail("cur_interval not specified in get_interval_data()\n"); } # the hash to return my %hash; # which of the intervals are we operating on? # if we defined alpha, beta, gamma ... alpha = 0, beta = 1, gamma = 2 my $interval_num; # the highest possible number for the current interval context # if we are working on alpha, and alpha is set to 6, this would be # equal to 5 (since we start at 0) my $interval_max; # this is the name of the previous interval, in relation to the one we're # working on. e.g., if we're operating on gamma, this should be "beta" my $prev_interval; # same as $interval_max, except for the previous interval. # this is used to determine which of the previous snapshots to pull from # e.g., cp -al alpha.$prev_interval_max/ beta.0/ my $prev_interval_max; # FIGURE OUT WHICH INTERVAL WE'RE RUNNING, AND HOW IT RELATES TO THE OTHERS # THEN RUN THE ACTION FOR THE CHOSEN INTERVAL # remember, in each hashref in this loop: # "interval" is something like "beta", "gamma", etc. # "number" is the number of these intervals to keep on the filesystem my $i = 0; foreach my $i_ref (@intervals) { # this is the interval we're set to run if ($$i_ref{'interval'} eq $cur_interval) { $interval_num = $i; # how many of these intervals should we keep? # we start counting from 0, so subtract one # e.g., 6 intervals == interval.0 .. interval.5 $interval_max = $$i_ref{'number'} - 1; # we found our interval, exit the foreach loop last; } # since the "last" command above breaks from this entire block, # and since we loop through the intervals in order, if we got this # far in the first place it means that we're looking at an interval # which isn't selected to run, and that there will be more intervals in the loop. # therefore, this WILL be the previous interval's information, the next time through. # $prev_interval = $$i_ref{'interval'}; # which of the previous interval's numbered directories should we pull from # for the interval we're currently set to run? # e.g., beta.0/ might get pulled from alpha.6/ # $prev_interval_max = $$i_ref{'number'} - 1; $i++; } # make sure we got something that makes sense if ($cur_interval ne 'sync') { if (!defined($interval_num)) { bail("Interval \"$cur_interval\" unknown, check $config_file"); } } # populate our hash $hash{'interval'} = $cur_interval; $hash{'interval_num'} = $interval_num; $hash{'interval_max'} = $interval_max; $hash{'prev_interval'} = $prev_interval; $hash{'prev_interval_max'} = $prev_interval_max; # and return the values return (\%hash); } # accepts no arguments # prints the most recent snapshot directory and exits # this is for use with the get-latest-snapshot command line argument sub show_latest_snapshot { # this should only be called after parse_config_file(), but just in case... if (!@intervals) { bail("Error! intervals not defined in show_latest_snapshot()"); } if (!%config_vars) { bail("Error! config_vars not defined in show_latest_snapshot()"); } # regardless of .sync, this is the latest "real" snapshot print $config_vars{'snapshot_root'} . '/' . $intervals[0]->{'interval'} . '.0/' . "\n"; exit(0); } # accepts no args # prints out status to the logs, then exits the program with the current exit code sub exit_with_status { if (0 == $exit_code) { syslog_msg("$run_string: completed successfully"); log_msg("$run_string: completed successfully", 2); exit($exit_code); } elsif (1 == $exit_code) { syslog_err("$run_string: completed, but with some errors"); log_err("$run_string: completed, but with some errors", 2); exit($exit_code); } elsif (2 == $exit_code) { syslog_warn("$run_string: completed, but with some warnings"); log_warn("$run_string: completed, but with some warnings", 2); exit($exit_code); } # this should never happen else { syslog_err("$run_string: completed, but with no definite status"); log_err("$run_string: completed, but with no definite status", 2); exit(1); } } # accepts no arguments # returns nothing # # exits the program with the status of the config file (e.g., Syntax OK). # the exit code is 0 for success, 1 for failure (although failure should never happen) sub exit_configtest { # if we're just doing a configtest, exit here with the results if (1 == $do_configtest) { if (1 == $config_perfect) { print "Syntax OK\n"; exit(0); } # this should never happen, because any errors should have killed the program before now else { print "Syntax Error\n"; exit(1); } } } # accepts no arguments # prints out error messages since we can't find the config file # exits with a return code of 1 sub exit_no_config_file { if (-d $config_file) { print STDERR "Can't read the config file: \"$config_file\" is a directory.\n"; if (0 == $do_configtest) { syslog_err("Can't read the config file: \"$config_file\" is a directory.\n"); } } else { # warn that the config file could not be found print STDERR "Config file \"$config_file\" does not exist or is not readable.\n"; if (0 == $do_configtest) { syslog_err("Config file \"$config_file\" does not exist or is not readable."); } } # if we have the default config from the install, remind the user to create the real config if ((-e "$config_file.default") && (!-e "$config_file")) { print STDERR "Did you copy $config_file.default to $config_file yet?\n"; } # exit showing an error exit(1); } # accepts a loglevel # returns 1 if it's valid, 0 otherwise sub is_valid_loglevel { my $value = shift(@_); if (!defined($value)) { return (0); } if ($value =~ m/^\d$/) { if (($value >= 1) && ($value <= 5)) { return (1); } } return (0); } # accepts a positive number formatted as string # returns 1 if it's valid, 0 otherwise sub is_valid_rsync_numtries { my $value = shift(@_); if (!defined($value)) { return (0); } if ($value =~ m/^\d+$/) { if (($value >= 0)) { return (1); } } } # accepts one argument # checks to see if that argument is set to 1 or 0 # returns 1 on success, 0 on failure sub is_boolean { my $var = shift(@_); if (!defined($var)) { return (0); } if ($var !~ m/^\d+$/) { return (0); } if (1 == $var) { return (1); } if (0 == $var) { return (1); } return (0); } # accepts string # returns 1 if it is a comment line (beginning with #) # returns 0 otherwise sub is_comment { my $str = shift(@_); if (!defined($str)) { return (undef); } if ($str =~ m/^#/) { return (1); } return (0); } # accepts string # returns 1 if it is blank, or just pure white space # returns 0 otherwise sub is_blank { my $str = shift(@_); if (!defined($str)) { return (undef); } if ($str !~ m/\S/) { return (1); } return (0); } # accepts path # returns 1 if it's a valid ssh path # returns 0 otherwise sub is_ssh_path { my $path = shift(@_); if (!defined($path)) { return (undef); } # make sure we don't have leading/trailing spaces if ($path =~ m/^\s/) { return (undef); } if ($path =~ m/\s$/) { return (undef); } # don't match paths that look like URIs (rsync://, etc.) if ($path =~ m,://,) { return (undef); } # must have [user@]host:[~.]/path syntax for ssh if ($path =~ m/^(.*?\@)?.*?:[~.]?\/.*$/) { return (1); } return (0); } # accepts path # returns 1 if it's a valid cwrsync server path (user@host::sharename) # return 0 otherwise sub is_cwrsync_path { my $path = shift(@_); if (!defined($path)) { return (undef); } if ($path =~ m/^[^\/]+::/) { return (1); } return (0); } # accepts path # returns 1 if it's a syntactically valid anonymous rsync path # returns 0 otherwise sub is_anon_rsync_path { my $path = shift(@_); if (!defined($path)) { return (undef); } if ($path =~ m/^rsync:\/\/.*$/) { return (1); } return (0); } # accepts path # returns 1 if it's a syntactically valid LVM path # returns 0 otherwise sub is_linux_lvm_path { my $path = shift(@_); if (!defined($path)) { return (undef); } if ($path =~ m|^lvm://.*$|) { return (1); } return (0); } # accepts proposed list for rsync_short_args # makes sure that rsync_short_args is in the format '-abcde' # (not '-a -b' or '-ab c', etc) # returns 1 if it's OK, or 0 otherwise sub is_valid_rsync_short_args { my $rsync_short_args = shift(@_); if (!defined($rsync_short_args)) { return (0); } # no blank space allowed if ($rsync_short_args =~ m/\s/) { return (0); } # first character must be a dash, followed by alphanumeric characters if ($rsync_short_args !~ m/^\-{1,1}\w+$/) { return (0); } return (1); } # accepts path # returns 1 if it's a real absolute path that currently exists # returns 0 otherwise sub is_real_local_abs_path { my $path = shift(@_); if (!defined($path)) { return (undef); } if (1 == is_valid_local_abs_path($path)) { # check for symlinks first, since they might not link to a real file if ((-l "$path") or (-e "$path")) { return (1); } } return (0); } # accepts path # returns 1 if it's a syntactically valid absolute path # returns 0 otherwise sub is_valid_local_abs_path { my $path = shift(@_); if (!defined($path)) { return (undef); } if ($path =~ m/^\//) { if (0 == is_directory_traversal($path)) { return (1); } } return (0); } # accepts path # returns 1 if it's a syntactically valid non-absolute (relative) path # returns 0 otherwise # does not check for directory traversal, since we want to use # a different error message if there is ".." in the path sub is_valid_local_non_abs_path { my $path = shift(@_); if (!defined($path)) { return (0); } if ($path =~ m/^\//) { return (0); # Absolute path => bad } if ($path =~ m/^\S/) { return (1); # Starts with a non-whitespace => good } else { return (0); # Empty or starts with whitespace => bad } } # accepts path # returns 1 if it's a directory traversal attempt # returns 0 if it's safe sub is_directory_traversal { my $path = shift(@_); if (!defined($path)) { return (undef); } # /.. if ($path =~ m/\/\.\./) { return (1); } # ../ if ($path =~ m/\.\.\//) { return (1); } return (0); } # accepts path # returns 1 if it's a file (doesn't have a trailing slash) # returns 0 otherwise sub is_file { my $path = shift(@_); if (!defined($path)) { return (undef); } if ($path !~ m/\/$/o) { return (1); } return (0); } # accepts path # returns 1 if it's a directory (has a trailing slash) # returns 0 otherwise sub is_directory { my $path = shift(@_); if (!defined($path)) { return (undef); } if ($path =~ m/\/$/o) { return (1); } return (0); } # accepts a string with a script file and optional arguments # returns 1 if it the script file exists, is executable and has absolute path. # returns 0 otherwise sub is_valid_script { my $full_script = shift(@_); # script to run (including args) my $script_ref = shift(@_); # reference to script file name my $script; # script file (no args) my @script_argv; # all script arguments # get the base name of the script, not counting any arguments to it @script_argv = split(/\s+/, $full_script); $script = $script_argv[0]; $$script_ref = $script; # Output $script in case caller wants it # make sure script exists and is executable if (-f "$script" && -x "$script" && is_real_local_abs_path($script)) { return 1; } return 0; } # accepts string # removes trailing slash, returns the string sub remove_trailing_slash { my $str = shift(@_); # it's not a trailing slash if it's the root filesystem if ($str eq '/') { return ($str); } # it's not a trailing slash if it's a remote root filesystem if ($str =~ m%:/$%) { return ($str); } $str =~ s/\/+$//; return ($str); } # accepts string # partially normalizes file path intended to be prefixed with some other path (such as backup dest) # does not handle symlinks or '..' sub normalize_dest_file_path_part { my $str = shift(@_); # it's not a trailing slash if it's the root filesystem if ($str eq '/') { return ($str); } $str =~ s/^\.\/|\/\.\/|\.$/\//g; $str =~ s/\/+/\//g; return ($str); } # accepts the interval (cmd) to run against # returns nothing # calls the appropriate subroutine, depending on whether this is the lowest interval or a higher one # also calls preexec/postexec scripts if we're working on the lowest interval # sub handle_interval { my $cmd = shift(@_); if (!defined($cmd)) { bail('cmd not defined in handle_interval()'); } my $id_ref = get_interval_data($cmd); my $result = 0; # here we used to check for interval.delete directories. This was # removed when we switched to using _delete.$$ directories. This # was done so that you can run another (eg) rsnapshot alpha, while # the .delete directory from the previous alpha backup was still # going. Potentially you may have several parallel deletes going on # with the new scheme, but I'm pretty sure that you'll catch up # eventually and not hopelessly wedge the machine -- DRC # handle toggling between sync_first being enabled and disabled # link_dest is enabled if (1 == $link_dest) { # sync_first is enabled if ($config_vars{'sync_first'}) { # create the sync root if it doesn't exist (and we need it right now) if ($cmd eq 'sync') { # don't create the .sync directory, it gets created later on } } # sync_first is disabled else { # if the sync directory is still here after sync_first is disabled, delete it if (-d "$config_vars{'snapshot_root'}/.sync") { display_rm_rf("$config_vars{'snapshot_root'}/.sync/"); if (0 == $test) { $result = rm_rf("$config_vars{'snapshot_root'}/.sync/"); if (0 == $result) { bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/.sync/\")"); } } } } } # link_dest is disabled else { # sync_first is enabled if ($config_vars{'sync_first'}) { # create the sync root if it doesn't exist if (!-d "$config_vars{'snapshot_root'}/.sync") { # If .sync does not exist but lowest.0 does, then copy that. # call generic cp_al() subroutine my $interval_0 = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0"; my $sync_dir = "$config_vars{'snapshot_root'}/.sync"; if (-d $interval_0) { display_cp_al("$interval_0", "$sync_dir"); if (0 == $test) { $result = cp_al("$interval_0", "$sync_dir"); if (!$result) { bail("Error! cp_al(\"$interval_0\", \"$sync_dir\")"); } } } } } # sync_first is disabled else { # if the sync directory still exists, delete it if (-d "$config_vars{'snapshot_root'}/.sync") { display_rm_rf("$config_vars{'snapshot_root'}/.sync/"); if (0 == $test) { $result = rm_rf("$config_vars{'snapshot_root'}/.sync/"); if (0 == $result) { bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/.sync/\")"); } } } } } # # now that the preliminaries are out of the way, the main backups happen here # # backup the lowest interval (or sync content to staging area) # we're not sure yet going in whether we'll be doing an actual backup, or just rotating snapshots for the lowest interval if ((defined($$id_ref{'interval_num'}) && (0 == $$id_ref{'interval_num'})) or ($cmd eq 'sync')) { # if we're doing a sync, run the pre/post exec scripts, and do the backup if ($cmd eq 'sync') { exec_cmd_preexec(); backup_lowest_interval($id_ref); exec_cmd_postexec(); } # if we're working on the lowest interval, either run the backup and rotate the snapshots, or just rotate them # (depending on whether sync_first is enabled else { if ($config_vars{'sync_first'}) { rotate_lowest_snapshots($$id_ref{'interval'}); } else { exec_cmd_preexec(); rotate_lowest_snapshots($$id_ref{'interval'}); backup_lowest_interval($id_ref); exec_cmd_postexec(); } } } # just rotate the higher intervals else { # this is not the most frequent unit, just rotate rotate_higher_interval($id_ref); } # if use_lazy_delete is on, delete the _delete.$$ directory if ($use_lazy_deletes) { # Besides the _delete.$$ directory, the lockfile has to be removed as well. # The reason is that the last task to do in this subroutine is to delete the _delete.$$ directory, and it can take quite a while. # we remove the lockfile here since this delete shouldn't block other rsnapshot jobs from running remove_lockfile(); # Check for the directory. It might not exist, e.g. in case of the 'sync' command. if (-d "$config_vars{'snapshot_root'}/_delete.$$") { # start the delete display_rm_rf("$config_vars{'snapshot_root'}/_delete.$$"); if (0 == $test) { my $result = rm_rf("$config_vars{'snapshot_root'}/_delete.$$"); if (0 == $result) { bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/_delete.$$\")\n"); } } } else { # only spit this out if lazy deletes are turned on. # Still need to suppress this if they're turned on but we've # not done enough backups to yet need to delete anything print_msg("No directory to delete: $config_vars{'snapshot_root'}/_delete.$$", 5); } } } # accepts an interval_data_ref # acts on the interval defined as $$id_ref{'interval'} (e.g., alpha) # this should be the smallest interval (e.g., alpha, not beta) # # rotates older dirs within this interval, hard links .0 to .1, # and rsync data over to .0 # # does not return a value, it bails instantly if there's a problem sub backup_lowest_interval { my $id_ref = shift(@_); # this should never happen if (!defined($id_ref)) { bail('backup_lowest_interval() expects an argument'); } if (!defined($$id_ref{'interval'})) { bail('backup_lowest_interval() expects an interval'); } # this also should never happen if ($$id_ref{'interval'} ne 'sync') { if (!defined($$id_ref{'interval_num'}) or (0 != $$id_ref{'interval_num'})) { bail('backup_lowest_interval() can only operate on the lowest interval'); } } my $sync_dest_matches = 0; my $sync_dest_dir = undef; # if we're trying to sync only certain directories, remember the path to match if ($ARGV[1]) { $sync_dest_dir = $ARGV[1]; } # sync live filesystem data and backup script output to $interval.0 # loop through each backup point, backup exec, and backup script foreach my $bp_ref (@backup_points) { # rsync the given backup point into the snapshot root if (defined($$bp_ref{'dest'}) && (defined($$bp_ref{'src'}) or defined($$bp_ref{'script'}))) { # if we're doing a sync and we specified an parameter on the command line (for the destination path), # only sync directories matching the destination path if (($$id_ref{'interval'} eq 'sync') && (defined($sync_dest_dir))) { my $avail_path = remove_trailing_slash($$bp_ref{'dest'}); my $req_path = remove_trailing_slash($sync_dest_dir); # if we have a match, sync this entry if ($avail_path eq $req_path) { # rsync if ($$bp_ref{'src'}) { rsync_backup_point($$id_ref{'interval'}, $bp_ref); # backup_script } elsif ($$bp_ref{'script'}) { exec_backup_script($$id_ref{'interval'}, $bp_ref); } # ok, we got at least one dest match $sync_dest_matches++; } } # this is a normal operation, either a sync or a lowest interval sync/rotate else { # rsync if ($$bp_ref{'src'}) { rsync_backup_point($$id_ref{'interval'}, $bp_ref); # backup_script } elsif ($$bp_ref{'script'}) { exec_backup_script($$id_ref{'interval'}, $bp_ref); } } # run a simple command } elsif (defined($$bp_ref{'cmd'})) { my $rc = exec_cmd($$bp_ref{'cmd'}); if ($rc != 0) { if ($$bp_ref{'importance'} eq 'required') { bail("\"$$bp_ref{'cmd'}\" returned \"$rc\". Exiting."); } else { print_warn("\"$$bp_ref{'cmd'}\" returned \"$rc\"", 2); } } } # this should never happen else { bail('invalid backup point data in backup_lowest_interval()'); } } if ($$id_ref{'interval'} eq 'sync') { if (defined($sync_dest_dir) && (0 == $sync_dest_matches)) { bail("No matches found for \"$sync_dest_dir\""); } } # rollback failed backups rollback_failed_backups($$id_ref{'interval'}); # update mtime on $interval.0/ to show when the snapshot completed touch_interval_dir($$id_ref{'interval'}); } # accepts $interval # returns nothing # # operates on directories in the given interval (it should be the lowest one) # deletes the highest numbered directory in the interval, and rotates the ones below it # if link_dest is enabled, .0 gets moved to .1 # otherwise, we do cp -al .0 .1 # # if we encounter an error, this script will terminate the program with an error condition # sub rotate_lowest_snapshots { my $interval = shift(@_); if (!defined($interval)) { bail('interval not defined in rotate_lowest_snapshots()'); } my $id_ref = get_interval_data($interval); my $interval_num = $$id_ref{'interval_num'}; my $interval_max = $$id_ref{'interval_max'}; my $prev_interval = $$id_ref{'prev_interval'}; my $prev_interval_max = $$id_ref{'prev_interval_max'}; my $result; # remove oldest directory if ((-d "$config_vars{'snapshot_root'}/$interval.$interval_max") && ($interval_max > 0)) { # if use_lazy_deletes is set move the oldest directory to _delete.$$ if (1 == $use_lazy_deletes) { print_cmd( "mv", "$config_vars{'snapshot_root'}/$interval.$interval_max/", "$config_vars{'snapshot_root'}/_delete.$$/" ); if (0 == $test) { my $result = safe_rename("$config_vars{'snapshot_root'}/$interval.$interval_max", "$config_vars{'snapshot_root'}/_delete.$$"); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\", \""; $errstr .= "$config_vars{'snapshot_root'}/_delete.$$/\")"; bail($errstr); } } } # otherwise the default is to delete the oldest directory for this interval else { display_rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/"); if (0 == $test) { my $result = rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/"); if (0 == $result) { bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\")\n"); } } } } # rotate the middle ones if ($interval_max > 0) { # Have we rotated a directory for this interval? my $dir_rotated = 0; for (my $i = ($interval_max - 1); $i > 0; $i--) { if (-d "$config_vars{'snapshot_root'}/$interval.$i") { print_cmd( "mv", "$config_vars{'snapshot_root'}/$interval.$i/ ", "$config_vars{'snapshot_root'}/$interval." . ($i + 1) . "/" ); if (0 == $test) { my $result = safe_rename( "$config_vars{'snapshot_root'}/$interval.$i", ("$config_vars{'snapshot_root'}/$interval." . ($i + 1)) ); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$i/\", \""; $errstr .= "$config_vars{'snapshot_root'}/$interval." . ($i + 1) . '/' . "\")"; bail($errstr); } } $dir_rotated = 1; } elsif ($dir_rotated) { # We have rotated a directory for this interval, but $i # does not exist - that probably means a hole. print_msg("Note: $config_vars{'snapshot_root'}/$interval.$i missing, cannot rotate it", 4); } } } # .0 and .1 require more attention, especially now with link_dest and sync_first # sync_first enabled if ($config_vars{'sync_first'}) { # we move .0 to .1 no matter what (assuming it exists) if (-d "$config_vars{'snapshot_root'}/$interval.0/") { print_cmd( "mv", "$config_vars{'snapshot_root'}/$interval.0/", "$config_vars{'snapshot_root'}/$interval.1/" ); if (0 == $test) { my $result = safe_rename("$config_vars{'snapshot_root'}/$interval.0", "$config_vars{'snapshot_root'}/$interval.1"); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.0/\", \""; $errstr .= "$config_vars{'snapshot_root'}/$interval.1/\")"; bail($errstr); } } } # if we're using rsync --link-dest, we need to mv sync to .0 now if (1 == $link_dest) { # mv sync .0 if (-d "$config_vars{'snapshot_root'}/.sync") { print_cmd( "mv", "$config_vars{'snapshot_root'}/.sync/", "$config_vars{'snapshot_root'}/$interval.0/" ); if (0 == $test) { my $result = safe_rename("$config_vars{'snapshot_root'}/.sync", "$config_vars{'snapshot_root'}/$interval.0"); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/.sync/\", \""; $errstr .= "$config_vars{'snapshot_root'}/$interval.0/\")"; bail($errstr); } } } } # otherwise, we hard link (except for directories, symlinks, and special files) sync to .0 else { # cp -al .sync .0 if (-d "$config_vars{'snapshot_root'}/.sync/") { display_cp_al("$config_vars{'snapshot_root'}/.sync/", "$config_vars{'snapshot_root'}/$interval.0/"); if (0 == $test) { $result = cp_al("$config_vars{'snapshot_root'}/.sync", "$config_vars{'snapshot_root'}/$interval.0"); if (!$result) { bail( "Error! cp_al(\"$config_vars{'snapshot_root'}/.sync\", \"$config_vars{'snapshot_root'}/$interval.0\")" ); } } } } # sync_first disabled (make sure we have a .0 directory and someplace to put it) } elsif ((-d "$config_vars{'snapshot_root'}/$interval.0") && ($interval_max > 0)) { # if we're using rsync --link-dest, we need to mv .0 to .1 now if (1 == $link_dest) { # move .0 to .1 if (-d "$config_vars{'snapshot_root'}/$interval.0/") { print_cmd( "mv $config_vars{'snapshot_root'}/$interval.0/ $config_vars{'snapshot_root'}/$interval.1/"); if (0 == $test) { my $result = safe_rename("$config_vars{'snapshot_root'}/$interval.0", "$config_vars{'snapshot_root'}/$interval.1"); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.0/\", "; $errstr .= "\"$config_vars{'snapshot_root'}/$interval.1/\")"; bail($errstr); } } } } # otherwise, we hard link (except for directories, symlinks, and special files) .0 over to .1 else { # call generic cp_al() subroutine if (-d "$config_vars{'snapshot_root'}/$interval.0/") { display_cp_al("$config_vars{'snapshot_root'}/$interval.0", "$config_vars{'snapshot_root'}/$interval.1"); if (0 == $test) { $result = cp_al("$config_vars{'snapshot_root'}/$interval.0/", "$config_vars{'snapshot_root'}/$interval.1/"); if (!$result) { my $errstr = ''; $errstr .= "Error! cp_al(\"$config_vars{'snapshot_root'}/$interval.0/\", "; $errstr .= "\"$config_vars{'snapshot_root'}/$interval.1/\")"; bail($errstr); } } } } } } # accepts interval, backup_point_ref, ssh_rsync_args_ref # returns no args # runs rsync on the given backup point # this is only run on the lowest points, not for rotations sub rsync_backup_point { my $interval = shift(@_); my $bp_ref = shift(@_); # validate subroutine args if (!defined($interval)) { bail('interval not defined in rsync_backup_point()'); } if (!defined($bp_ref)) { bail('bp_ref not defined in rsync_backup_point()'); } if (!defined($$bp_ref{'src'})) { bail('src not defined in rsync_backup_point()'); } if (!defined($$bp_ref{'dest'})) { bail('dest not defined in rsync_backup_point()'); } # set up default args for rsync and ssh my $ssh_args = $default_ssh_args; my $rsync_short_args = $default_rsync_short_args; my $rsync_long_args = $default_rsync_long_args; # other misc variables my @cmd_stack = undef; my $src = $$bp_ref{'src'}; my $result = undef; my $linux_lvm_oldpwd = undef; my $lvm_src = undef; # if we're using link-dest later, that target depends on whether we're doing a 'sync' or a regular interval # if we're doing a "sync", then look at [lowest-interval].0 instead of [cur-interval].1 my $interval_link_dest; my $interval_num_link_dest; # start looking for link_dest targets at interval.$start_num my $start_num = 1; # if we're doing a sync, we'll start looking at [lowest-interval].0 for a link_dest target if ($interval eq 'sync') { $start_num = 0; } # look for the most recent link_dest target directory # loop through all snapshots until we find the first match foreach my $i_ref (@intervals) { if (defined($$i_ref{'number'})) { for (my $i = $start_num; $i < $$i_ref{'number'}; $i++) { my $i_check; if ($test && $interval ne 'sync') { # A real run would already have rotated the snapshots up, but this test run hasn't. # Hence, to know whether $i would exist at this point of a real run, we must check for $i - 1. $i_check = $i - 1; } else { $i_check = $i; } # once we find a valid link_dest target, the search is over if (-e "$config_vars{'snapshot_root'}/$$i_ref{'interval'}.$i_check/$$bp_ref{'dest'}") { if (!defined($interval_link_dest) && !defined($interval_num_link_dest)) { $interval_link_dest = $$i_ref{'interval'}; $interval_num_link_dest = $i; } # we'll still loop through the outer loop a few more times, but the defined() check above # will make sure the first match wins last; } } } } # check to see if this destination path has already failed # if it's set to be rolled back, skip out now foreach my $rollback_point (@rollback_points) { if (defined($rollback_point)) { my $tmp_dest = $$bp_ref{'dest'}; my $tmp_rollback_point = $rollback_point; # don't compare the slashes at the end $tmp_dest = remove_trailing_slash($tmp_dest); $tmp_rollback_point = remove_trailing_slash($tmp_rollback_point); if ("$tmp_dest" eq "$tmp_rollback_point") { print_warn("$src skipped due to rollback plan", 2); syslog_warn("$src skipped due to rollback plan"); return (undef); } } } # if the config file specified rsync or ssh args, use those instead of the hard-coded defaults in the program if (defined($config_vars{'rsync_short_args'})) { $rsync_short_args = $config_vars{'rsync_short_args'}; } if (defined($config_vars{'rsync_long_args'})) { $rsync_long_args = $config_vars{'rsync_long_args'}; } if (defined($config_vars{'ssh_args'})) { $ssh_args = $config_vars{'ssh_args'}; } # extra verbose? if ($verbose > 3) { $rsync_short_args .= 'v'; } # split up rsync long args into an array, paying attention to # quoting - ideally we'd use Text::Balanced or similar, but that's # only relatively recently gone into core my @rsync_long_args_stack = split_long_args_with_quotes('rsync_long_args', $rsync_long_args); # create $interval.0/$$bp_ref{'dest'} or .sync/$$bp_ref{'dest'} directory if it doesn't exist # (this may create the .sync dir, which is why we had to check for it above) # create_backup_point_dir($interval, $bp_ref); # check opts, first unique to this backup point, and then global # # with all these checks, we try the local option first, and if # that isn't specified, we attempt to use the global setting as # a fallback plan # # we do the rsync args first since they overwrite the rsync_* variables, # whereas the subsequent options append to them # # RSYNC SHORT ARGS if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'rsync_short_args'})) { $rsync_short_args = $$bp_ref{'opts'}->{'rsync_short_args'}; } if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'extra_rsync_short_args'})) { $rsync_short_args .= '-' if (!$rsync_short_args); $rsync_short_args .= substr $$bp_ref{'opts'}->{'extra_rsync_short_args'}, 1; } # RSYNC LONG ARGS if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'rsync_long_args'})) { @rsync_long_args_stack = split_long_args_with_quotes('rsync_long_args (for a backup point)', $$bp_ref{'opts'}->{'rsync_long_args'}); } if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'extra_rsync_long_args'})) { push( @rsync_long_args_stack, split_long_args_with_quotes( 'extra_rsync_long_args (for a backup point)', $$bp_ref{'opts'}->{'extra_rsync_long_args'} ) ); } # SSH ARGS if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'ssh_args'})) { $ssh_args = $$bp_ref{'opts'}->{'ssh_args'}; } if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'extra_ssh_args'})) { $ssh_args .= ' ' . $$bp_ref{'opts'}->{'extra_ssh_args'}; } # ONE_FS if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'one_fs'})) { if (1 == $$bp_ref{'opts'}->{'one_fs'}) { $rsync_short_args .= 'x'; } } elsif ($one_fs) { $rsync_short_args .= 'x'; } # SEE WHAT KIND OF SOURCE WE'RE DEALING WITH # # local filesystem if (is_real_local_abs_path($src)) { # no change # if this is a user@host:/path (or ...:./path, or ...:~/...), use ssh } elsif (is_ssh_path($src)) { # if we have any args for SSH, add them if (defined($ssh_args)) { push(@rsync_long_args_stack, "--rsh=$config_vars{'cmd_ssh'} $ssh_args"); } # no arguments is the default else { push(@rsync_long_args_stack, "--rsh=$config_vars{'cmd_ssh'}"); } # anonymous rsync } elsif (is_anon_rsync_path($src)) { # make rsync quiet if we're running in quiet mode if ($verbose < 2) { $rsync_short_args .= 'q'; } # cwrsync path } elsif (is_cwrsync_path($src)) { # make rsync quiet if we're running in quiet mode if ($verbose < 2) { $rsync_short_args .= 'q'; } # LVM path } elsif (is_linux_lvm_path($src)) { # take LVM snapshot and mount, reformat src into local path unless (defined($config_vars{'linux_lvm_snapshotsize'})) { bail("Missing required argument for LVM source: linux_lvm_snapshotsize"); } unless (defined($config_vars{'linux_lvm_snapshotname'})) { bail("Missing required argument for LVM source: linux_lvm_snapshotname"); } unless (defined($config_vars{'linux_lvm_vgpath'})) { bail("Missing required argument for LVM source: linux_lvm_vgpath"); } unless (defined($config_vars{'linux_lvm_mountpath'})) { bail("Missing required argument for LVM source: linux_lvm_mountpath"); } $lvm_src = $src; linux_lvm_snapshot_create(linux_lvm_parseurl($lvm_src)); $traps{"linux_lvm_snapshot"} = $lvm_src; linux_lvm_mount(linux_lvm_parseurl($lvm_src)); $traps{"linux_lvm_mountpoint"} = 1; # rewrite src to point to mount path # - to avoid including the mountpath in the snapshot, change the working directory and use a relative source $linux_lvm_oldpwd = cwd(); print_cmd("chdir($config_vars{'linux_lvm_mountpath'})"); if (0 == $test) { $result = chdir($config_vars{'linux_lvm_mountpath'}); if (0 == $result) { bail("Could not change directory to \"$config_vars{'linux_lvm_mountpath'}\""); } } $src = './' . (linux_lvm_parseurl($lvm_src))[2]; } # this should have already been validated once, but better safe than sorry else { bail("Could not understand source \"$src\" in backup_lowest_interval()"); } # if we're using --link-dest, we'll need to specify the link-dest directory target # this varies depending on whether we're operating on the lowest interval or doing a 'sync' if (1 == $link_dest) { # bp_ref{'dest'} and snapshot_root have already been validated, but these might be blank if (defined($interval_link_dest) && defined($interval_num_link_dest)) { # push link_dest arguments onto cmd stack push(@rsync_long_args_stack, "--link-dest=$config_vars{'snapshot_root'}/$interval_link_dest.$interval_num_link_dest/$$bp_ref{'dest'}" ); } } # SPECIAL EXCEPTION: # If we're using --link-dest AND the source is a file AND we have a copy from the last time, # manually link interval.1/foo to interval.0/foo # # This is necessary because --link-dest only works on directories # if ( (1 == $link_dest) && (is_file($src)) && defined($interval_link_dest) && defined($interval_num_link_dest) && (-f "$config_vars{'snapshot_root'}/$interval_link_dest.$interval_num_link_dest/$$bp_ref{'dest'}") ) { # these are both "destination" paths, but we're moving from .1 to .0 my $srcpath; my $destpath; $srcpath = "$config_vars{'snapshot_root'}/$interval_link_dest.$interval_num_link_dest/$$bp_ref{'dest'}"; if ($interval eq 'sync') { $destpath = "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}"; } else { $destpath = "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}"; } print_cmd("ln $srcpath $destpath"); if (0 == $test) { $result = link("$srcpath", "$destpath"); if (!defined($result) or (0 == $result)) { print_err("link(\"$srcpath\", \"$destpath\") failed", 2); syslog_err("link(\"$srcpath\", \"$destpath\") failed"); } } } # put a trailing slash on the source if we know it's a directory and it doesn't have one if ((-d "$src") && ($$bp_ref{'src'} !~ /\/$/)) { $src .= '/'; } # BEGIN RSYNC COMMAND ASSEMBLY # take care not to introduce blank elements into the array, # since it can confuse rsync, which in turn causes strange errors # @cmd_stack = (); # # rsync command push(@cmd_stack, $config_vars{'cmd_rsync'}); # # rsync short args if (defined($rsync_short_args) && ($rsync_short_args ne '')) { push(@cmd_stack, $rsync_short_args); } # # rsync long args if (@rsync_long_args_stack && (scalar(@rsync_long_args_stack) > 0)) { foreach my $tmp_long_arg (@rsync_long_args_stack) { if (defined($tmp_long_arg) && ($tmp_long_arg ne '')) { push(@cmd_stack, $tmp_long_arg); } } } # # src push(@cmd_stack, "$src"); # # dest if ($interval eq 'sync') { push(@cmd_stack, "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}"); } else { push(@cmd_stack, "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}"); } # # END RSYNC COMMAND ASSEMBLY # RUN THE RSYNC COMMAND FOR THIS BACKUP POINT BASED ON THE @cmd_stack VARS print_cmd(@cmd_stack); my $tryCount = 0; $result = 1; if (0 == $test) { while ($tryCount < $rsync_numtries && $result != 0) { # open rsync and capture STDOUT and STDERR # the 3rd argument is undefined, that STDERR gets mashed into STDOUT and we # don't have to care about getting both STREAMS together without mixing up time my ($rsync_in, $rsync_out); my $pid = open3($rsync_in, $rsync_out, undef, @cmd_stack) or die "Couldn't fork rsync: $!\n"; # add autoflush to get output by time and not at the end when rsync is finished $rsync_out->autoflush(); while (<$rsync_out>) { print_msg($_, 3); } waitpid($pid, 0); $result = get_retval($?); $tryCount += 1; } # now we see if rsync ran successfully, and what to do about it if ($result != 0) { # print warnings, and set this backup point to rollback if we're using --link-dest handle_rsync_error($result, $bp_ref); } else { print_msg("rsync succeeded", 5); } } if (1 == $traps{"linux_lvm_mountpoint"} || 0 ne $traps{"linux_lvm_snapshot"}) { print_cmd("chdir($linux_lvm_oldpwd)"); if (0 == $test) { $result = chdir($linux_lvm_oldpwd); if (0 == $result) { bail("Could not change directory to \"$linux_lvm_oldpwd\""); } } } # delte the traps manually # umount LVM Snapshot if it is mounted if (1 == $traps{"linux_lvm_mountpoint"}) { $traps{"linux_lvm_mountpoint"} = 0; linux_lvm_unmount(); } # destroy snapshot created by rsnapshot if (0 ne $traps{"linux_lvm_snapshot"}) { $traps{"linux_lvm_snapshot"} = 0; linux_lvm_snapshot_del(linux_lvm_parseurl($lvm_src)); } } # # split a LVM backup source into vgname volname and path # # 1. parameter: full LVM source # # returns: vgname, volname, path as array sub linux_lvm_parseurl() { my $src = shift @_; # parse LVM src ('lvm://vgname/volname/path') my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = ($src =~ m|^lvm://([^/]+)/([^/]+)/(.*)$|); # lvmvolname and/or path could be the string "0", so test for 'defined': unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) { bail("Could not understand LVM source \"$src\" in linux_lvm_parseurl()"); } return ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath); } # # assemble and execute LVM snapshot command # # parameters: the return of linux_lvm_parseurl() # # returns: - sub linux_lvm_snapshot_create { my $result = undef; my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = @_; unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) { bail("linux_lvm_snapshot_create needs 3 parameters!"); } my @cmd_stack = (); push(@cmd_stack, split(' ', $config_vars{'linux_lvm_cmd_lvcreate'})); push(@cmd_stack, '--snapshot'); push(@cmd_stack, '--size'); push(@cmd_stack, $config_vars{'linux_lvm_snapshotsize'}); push(@cmd_stack, '--name'); push(@cmd_stack, $config_vars{'linux_lvm_snapshotname'}); push(@cmd_stack, join('/', $config_vars{'linux_lvm_vgpath'}, $linux_lvmvgname, $linux_lvmvolname)); print_cmd(@cmd_stack); if (0 == $test) { # silence gratuitous lvcreate output #$result = system(@cmd_stack); $result = system(join " ", @cmd_stack, ">/dev/null"); if ($result != 0) { bail("Create LVM snapshot failed: $result"); } } } # # delete LVM-snapshot # # parameters: the return of linux_lvm_parseurl() # # returns: - sub linux_lvm_snapshot_del { my $result = undef; my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = @_; unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) { bail("linux_lvm_snapshot_del needs 3 parameters!"); } my @cmd_stack = (); push(@cmd_stack, $config_vars{'linux_lvm_cmd_lvremove'}); push(@cmd_stack, '--force'); push( @cmd_stack, join('/', $config_vars{'linux_lvm_vgpath'}, $linux_lvmvgname, $config_vars{'linux_lvm_snapshotname'}) ); print_cmd(@cmd_stack); if (0 == $test) { # silence gratuitous lvremove output #$result = system(@cmd_stack); $result = system(join " ", @cmd_stack, ">/dev/null"); if ($result != 0) { bail("Removal of LVM snapshot failed: $result"); } } } # # mount a LVM-snapshot # # parameters: the return of linux_lvm_parseurl() # # returns: - sub linux_lvm_mount { my $result = undef; my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = @_; unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) { bail("linux_lvm_mount needs 3 parameters!"); } # mount the snapshot my @cmd_stack = (); push(@cmd_stack, split(' ', $config_vars{'linux_lvm_cmd_mount'})); push( @cmd_stack, join('/', $config_vars{'linux_lvm_vgpath'}, $linux_lvmvgname, $config_vars{'linux_lvm_snapshotname'}) ); push(@cmd_stack, $config_vars{'linux_lvm_mountpath'}); print_cmd(@cmd_stack); if (0 == $test) { $result = system(@cmd_stack); if ($result != 0) { bail("Mount LVM snapshot failed: $result"); } } } # # unmount a LVM-snapshot # # parameters: - # # returns: - sub linux_lvm_unmount { my $result = undef; my @cmd_stack = (); push(@cmd_stack, split(' ', $config_vars{'linux_lvm_cmd_umount'})); push(@cmd_stack, $config_vars{'linux_lvm_mountpath'}); print_cmd(@cmd_stack); if (0 == $test) { $result = system(@cmd_stack); if ($result != 0) { bail("Unmount LVM snapshot failed: $result"); } } } # accepts the name of the argument to split, and its value # the name is used for spitting out error messages # # returns a list sub split_long_args_with_quotes { my ($argname, $argvalue) = @_; my $inquotes = ''; my @stack = (''); for (my $i = 0; $i < length($argvalue); $i++) { my $thischar = substr($argvalue, $i, 1); # got whitespace and not in quotes? end this argument, start next if ($thischar =~ /\s/ && !$inquotes) { $#stack++; next; # not in quotes and got a quote? remember that we're in quotes # NB the unnecessary \ are to appease emacs } elsif ($thischar =~ /[\'\"]/ && !$inquotes) { $inquotes = $thischar; # in quotes and got a different quote? no nesting allowed # more emacs appeasement } elsif ($thischar =~ /[\'\"]/ && $inquotes ne $thischar) { print_err("Nested quotes not allowed in $argname", 1); syslog_err("Nested quotes not allowed in $argname"); exit(1); # in quotes and got a close quote } elsif ($thischar eq $inquotes) { $inquotes = ''; } else { $stack[-1] .= $thischar; } } if ($inquotes) { print_err("Unbalanced quotes in $argname", 1); syslog_err("Unbalanced quotes in $argname"); exit(1); } return @stack; } # accepts rsync exit code, backup_point_ref # prints out an appropriate error message (and logs it) # also adds destination path to the rollback queue if link_dest is enabled sub handle_rsync_error { my $retval = shift(@_); my $bp_ref = shift(@_); # shouldn't ever happen if (!defined($retval)) { bail('retval undefined in handle_rsync_error()'); } if (0 == $retval) { bail('retval == 0 in handle_rsync_error()'); } if (!defined($bp_ref)) { bail('bp_ref undefined in handle_rsync_error()'); } # a partial list of rsync exit values (from the rsync 2.6.0 man page) # # 0 Success # 1 Syntax or usage error # 23 Partial transfer due to error # 24 Partial transfer due to vanished source files # # if we got error 1 and we were attempting --link-dest, there's # a very good chance that this version of rsync is too old. # if ((1 == $link_dest) && (1 == $retval)) { print_err( "$config_vars{'cmd_rsync'} syntax or usage error. Does this version of rsync support --link-dest?", 2 ); syslog_err( "$config_vars{'cmd_rsync'} syntax or usage error. Does this version of rsync support --link-dest?"); # 23 and 24 are treated as warnings because users might be using the filesystem during the backup # if you want perfect backups, don't allow the source to be modified while the backups are running :) } elsif (23 == $retval) { print_warn( "Some files and/or directories in $$bp_ref{'src'} only transferred partially during rsync operation", 2 ); syslog_warn( "Some files and/or directories in $$bp_ref{'src'} only transferred partially during rsync operation" ); } elsif (24 == $retval) { print_warn("Some files and/or directories in $$bp_ref{'src'} vanished during rsync operation", 4); syslog_warn("Some files and/or directories in $$bp_ref{'src'} vanished during rsync operation"); } # other error else { print_err("$config_vars{'cmd_rsync'} returned $retval while processing $$bp_ref{'src'}", 2); syslog_err("$config_vars{'cmd_rsync'} returned $retval while processing $$bp_ref{'src'}"); # set this directory to rollback if we're using link_dest # (since $interval.0/ will have been moved to $interval.1/ by now) if (1 == $link_dest) { push(@rollback_points, $$bp_ref{'dest'}); } } } # accepts interval, backup_point_ref, ssh_rsync_args_ref # returns no args # runs rsync on the given backup point sub exec_backup_script { my $interval = shift(@_); my $bp_ref = shift(@_); # validate subroutine args if (!defined($interval)) { bail('interval not defined in exec_backup_script()'); } if (!defined($bp_ref)) { bail('bp_ref not defined in exec_backup_script()'); } # other misc variables my $script = undef; my $tmpdir = undef; my $result = undef; # remember what directory we started in my $cwd = cwd(); # create $interval.0/$$bp_ref{'dest'} directory if it doesn't exist # create_backup_point_dir($interval, $bp_ref); # work in a temp dir, and make this the source for the rsync operation later # not having a trailing slash is a subtle distinction. it allows us to use # the same path if it's NOT a directory when we try to delete it. $tmpdir = "$config_vars{'snapshot_root'}/tmp"; # remove the tmp directory if it's still there for some reason # (this shouldn't happen unless the program was killed prematurely, etc) if (-e "$tmpdir") { display_rm_rf("$tmpdir/"); if (0 == $test) { $result = rm_rf("$tmpdir/"); if (0 == $result) { bail("Could not rm_rf(\"$tmpdir/\");"); } } } # create the tmp directory print_cmd("mkdir -m 0755 -p $tmpdir/"); if (0 == $test) { eval { # don't ever pass a trailing slash to mkpath mkpath("$tmpdir", 0, 0755); }; if ($@) { bail("Unable to create \"$tmpdir/\",\nPlease make sure you have the right permissions."); } } # no more calls to mkpath here. the tmp dir needs a trailing slash $tmpdir .= '/'; # change to the tmp directory print_cmd("cd $tmpdir"); if (0 == $test) { $result = chdir("$tmpdir"); if (0 == $result) { bail("Could not change directory to \"$tmpdir\""); } } # run the backup script # # the assumption here is that the backup script is written in such a way # that it creates files in its current working directory. # # the backup script should return 0 on success, anything else is # considered a failure. # print_cmd($$bp_ref{'script'}); if (0 == $test) { $result = system($$bp_ref{'script'}); if ($result != 0) { # bitmask return value my $retval = get_retval($result); print_err("backup_script $$bp_ref{'script'} returned $retval", 2); syslog_err("backup_script $$bp_ref{'script'} returned $retval"); # if the backup script failed, roll back to the last good data push(@rollback_points, $$bp_ref{'dest'}); } } # change back to the previous directory # (/ is a special case) if ('/' eq $cwd) { print_cmd("cd $cwd"); } else { print_cmd("cd $cwd/"); } if (0 == $test) { chdir($cwd); } # if we're using link_dest, pull back the previous files (as links) that were moved up if any. # this is because in this situation, .0 will always be empty, so we'll pull select things # from .1 back to .0 if possible. these will be used as a baseline for diff comparisons by # sync_if_different() down below. if (1 == $link_dest) { my $lastdir; my $curdir; if ($interval eq 'sync') { $lastdir = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0/$$bp_ref{'dest'}"; $curdir = "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}"; } else { $lastdir = "$config_vars{'snapshot_root'}/$interval.1/$$bp_ref{'dest'}"; $curdir = "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}"; } # make sure we have a slash at the end if ($lastdir !~ m/\/$/) { $lastdir .= '/'; } if ($curdir !~ m/\/$/) { $curdir .= '/'; } # if we even have files from last time if (-e "$lastdir") { # and we're not somehow clobbering an existing directory (shouldn't happen) if (!-e "$curdir") { # call generic cp_al() subroutine display_cp_al("$lastdir", "$curdir"); if (0 == $test) { $result = cp_al("$lastdir", "$curdir"); if (!$result) { print_err("Warning! cp_al(\"$lastdir\", \"$curdir/\")", 2); } } } } } # sync the output of the backup script into this snapshot interval # this is using a native function since rsync doesn't quite do what we want # # rsync doesn't work here because it sees that the timestamps are different, and # insists on changing things even if the files are bit for bit identical on content. # # check to see where we're syncing to my $target_dir; if ($interval eq 'sync') { $target_dir = "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}"; } else { $target_dir = "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}"; } print_cmd("sync_if_different(\"$tmpdir\", \"$target_dir\")"); if (0 == $test) { $result = sync_if_different("$tmpdir", "$target_dir"); if (!defined($result)) { print_err("Warning! sync_if_different(\"$tmpdir\", \"$$bp_ref{'dest'}\") returned undef", 2); } } # remove the tmp directory if (-e "$tmpdir") { display_rm_rf("$tmpdir"); if (0 == $test) { $result = rm_rf("$tmpdir"); if (0 == $result) { bail("Could not rm_rf(\"$tmpdir\");"); } } } } # accepts and runs an arbitrary command string # returns the exit value of the command sub exec_cmd { my $cmd = shift(@_); my $return = 0; my $retval = 0; if (!defined($cmd) or ('' eq $cmd)) { print_err("Warning! Command \"$cmd\" not found", 2); return (undef); } print_cmd($cmd); if (0 == $test) { my $pre_systemcall_cwd = cwd(); # run $cmd from $HOME, allows unmounting of the snapshot root by # cmd_postexec config option (se Debian Bug #660372) chdir(); $return = system($cmd); # return to the directory we were in before executing $cmd chdir($pre_systemcall_cwd); if (!defined($return)) { print_err("Warning! exec_cmd(\"$cmd\") returned undef", 2); } # bitmask to get the real return value $retval = get_retval($return); } return ($retval); } # accepts no arguments # returns the exit code of the defined preexec script, or undef if the command is not found sub exec_cmd_preexec { my $retval = 0; # exec_cmd will only run if we're not in test mode if (defined($config_vars{'cmd_preexec'})) { $retval = exec_cmd("$config_vars{'cmd_preexec'}"); } if (!defined($retval)) { print_err("$config_vars{'cmd_preexec'} not found", 2); } if (0 != $retval) { bail("cmd_preexec \"$config_vars{'cmd_preexec'}\" returned $retval"); } return ($retval); } # accepts no arguments # returns the exit code of the defined preexec script, or undef if the command is not found sub exec_cmd_postexec { my $retval = 0; # exec_cmd will only run if we're not in test mode if (defined($config_vars{'cmd_postexec'})) { $retval = exec_cmd("$config_vars{'cmd_postexec'}"); } if (!defined($retval)) { print_err("$config_vars{'cmd_postexec'} not found", 2); } if (0 != $retval) { bail("cmd_postexec \"$config_vars{'cmd_postexec'}\" returned $retval"); } return ($retval); } # accepts interval, backup_point_ref # returns nothing # exits the program if it encounters a fatal error sub create_backup_point_dir { my $interval = shift(@_); my $bp_ref = shift(@_); # validate subroutine args if (!defined($interval)) { bail('interval not defined in create_interval_0()'); } if (!defined($bp_ref)) { bail('bp_ref not defined in create_interval_0()'); } # create missing parent directories inside the $interval.x directory my @dirs = split(/\//, $$bp_ref{'dest'}); pop(@dirs); # don't mkdir for dest unless we have to my $destpath; if ($interval eq 'sync') { $destpath = "$config_vars{'snapshot_root'}/.sync/" . join('/', @dirs); } else { $destpath = "$config_vars{'snapshot_root'}/$interval.0/" . join('/', @dirs); } # make sure we DON'T have a trailing slash (for mkpath) if ($destpath =~ m/\/$/) { $destpath = remove_trailing_slash($destpath); } # create the directory if it doesn't exist if (!-e "$destpath") { print_cmd("mkdir -m 0755 -p $destpath/"); if (0 == $test) { eval { mkpath("$destpath", 0, 0755); }; if ($@) { bail("Could not mkpath(\"$destpath/\", 0, 0755);"); } } } } # accepts interval we're operating on # returns nothing important # rolls back failed backups, as defined in the @rollback_points array # this is necessary if we're using link_dest, since it moves the .0 to .1 directory, # instead of recursively copying links to the files. it also helps with failed # backup scripts. # sub rollback_failed_backups { my $interval = shift(@_); if (!defined($interval)) { bail('interval not defined in rollback_failed_backups()'); } my $result; my $rsync_short_args = $default_rsync_short_args; # handle 'sync' case my $interval_src; my $interval_dest; if ($interval eq 'sync') { $interval_src = $intervals[0]->{'interval'} . '.0'; $interval_dest = '.sync'; } else { $interval_src = "$interval.1"; $interval_dest = "$interval.0"; } # extra verbose? if ($verbose > 3) { $rsync_short_args .= 'v'; } # rollback failed backups (if we're using link_dest) foreach my $rollback_point (@rollback_points) { # make sure there's something to rollback from if (!-e "$config_vars{'snapshot_root'}/$interval_src/$rollback_point") { next; } print_warn("Rolling back \"$rollback_point\"", 2); syslog_warn("Rolling back \"$rollback_point\""); # using link_dest, this probably won't happen # just in case, we may have to delete the old backup point from interval.0 / .sync if (-e "$config_vars{'snapshot_root'}/$interval_dest/$rollback_point") { display_rm_rf("$config_vars{'snapshot_root'}/$interval_dest/$rollback_point"); if (0 == $test) { $result = rm_rf("$config_vars{'snapshot_root'}/$interval_dest/$rollback_point"); if (0 == $result) { bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/$interval_dest/$rollback_point\")\n"); } } } # copy hard links back from .1 to .0 # this will re-populate the .0 directory without taking up (much) additional space # # if we're doing a 'sync', then instead of .1 and .0, it's lowest.0 and .sync display_cp_al( "$config_vars{'snapshot_root'}/$interval_src/$rollback_point", "$config_vars{'snapshot_root'}/$interval_dest/$rollback_point" ); if (0 == $test) { $result = cp_al( "$config_vars{'snapshot_root'}/$interval_src/$rollback_point", "$config_vars{'snapshot_root'}/$interval_dest/$rollback_point" ); if (!$result) { my $errstr = ''; $errstr .= "Error! cp_al(\"$config_vars{'snapshot_root'}/$interval_src/$rollback_point\", "; $errstr .= "\"$config_vars{'snapshot_root'}/$interval_dest/$rollback_point\")"; bail($errstr); } } } } # accepts interval # returns nothing # updates mtime on $interval.0 sub touch_interval_dir { my $interval = shift(@_); if (!defined($interval)) { bail('interval not defined in touch_interval()'); } my $interval_dir; if ($interval eq 'sync') { $interval_dir = '.sync'; } else { $interval_dir = $interval . '.0'; } # update mtime of $interval.0 to reflect the time this snapshot was taken print_cmd("touch $config_vars{'snapshot_root'}/$interval_dir/"); if (0 == $test && -e "$config_vars{'snapshot_root'}/$interval_dir/") { my $result = utime(time(), time(), "$config_vars{'snapshot_root'}/$interval_dir/"); if (0 == $result) { bail("Could not utime(time(), time(), \"$config_vars{'snapshot_root'}/$interval_dir/\");"); } } } # accepts an interval_data_ref # looks at $$id_ref{'interval'} as the interval to act on, # and the previous interval $$id_ref{'prev_interval'} to pull up the directory from (e.g., beta, alpha) # the interval being acted upon should not be the lowest one. # # rotates older dirs within this interval, and hard links # the previous interval's highest numbered dir to this interval's .0, # # does not return a value, it bails instantly if there's a problem sub rotate_higher_interval { my $id_ref = shift(@_); # this should never happen if (!defined($id_ref)) { bail('rotate_higher_interval() expects an interval_data_ref'); } # this also should never happen if (!defined($$id_ref{'interval_num'}) or (0 == $$id_ref{'interval_num'})) { bail('rotate_higher_interval() can only operate on the higher intervals'); } # set up variables for convenience since we refer to them extensively my $interval = $$id_ref{'interval'}; my $interval_num = $$id_ref{'interval_num'}; my $interval_max = $$id_ref{'interval_max'}; my $prev_interval = $$id_ref{'prev_interval'}; my $prev_interval_max = $$id_ref{'prev_interval_max'}; # ROTATE DIRECTORIES # # delete the oldest one (if we're keeping more than one) if (-d "$config_vars{'snapshot_root'}/$interval.$interval_max") { # if use_lazy_deletes is set move the oldest directory to _delete.$$ # otherwise preform the default behavior if (1 == $use_lazy_deletes) { print_cmd( "mv ", "$config_vars{'snapshot_root'}/$interval.$interval_max/ ", "$config_vars{'snapshot_root'}/_delete.$$/" ); if (0 == $test) { my $result = safe_rename( "$config_vars{'snapshot_root'}/$interval.$interval_max", ("$config_vars{'snapshot_root'}/_delete.$$") ); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\", \""; $errstr .= "$config_vars{'snapshot_root'}/_delete.$$/\")"; bail($errstr); } } } else { display_rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/"); if (0 == $test) { my $result = rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/"); if (0 == $result) { bail("Could not rm_rf(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\");"); } } } } else { print_msg( "$config_vars{'snapshot_root'}/$interval.$interval_max not present (yet), nothing to delete", 4); } # rotate the middle ones for (my $i = ($interval_max - 1); $i >= 0; $i--) { if (-d "$config_vars{'snapshot_root'}/$interval.$i") { print_cmd( "mv $config_vars{'snapshot_root'}/$interval.$i/ ", "$config_vars{'snapshot_root'}/$interval." . ($i + 1) . "/" ); if (0 == $test) { my $result = safe_rename( "$config_vars{'snapshot_root'}/$interval.$i", ("$config_vars{'snapshot_root'}/$interval." . ($i + 1)) ); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$i/\", \""; $errstr .= "$config_vars{'snapshot_root'}/$interval." . ($i + 1) . '/' . "\")"; bail($errstr); } } } else { print_msg("$config_vars{'snapshot_root'}/$interval.$i not present (yet), nothing to rotate", 4); } } # prev.max and interval.0 require more attention if (-d "$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max") { my $result; # if the previous interval has at least 2 snapshots, # or if the previous interval isn't the smallest one, # move the last one up a level if (($prev_interval_max >= 1) or ($interval_num >= 2)) { # mv alpha.5 to beta.0 (or whatever intervals we're using) print_cmd("mv $config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max/ ", "$config_vars{'snapshot_root'}/$interval.0/"); if (0 == $test) { $result = safe_rename("$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max", "$config_vars{'snapshot_root'}/$interval.0"); if (0 == $result) { my $errstr = ''; $errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max/\", "; $errstr .= "\"$config_vars{'snapshot_root'}/$interval.0/\")"; bail($errstr); } } } else { print_err("$prev_interval must be above 1 to keep snapshots at the $interval level", 1); exit(1); } } else { print_msg( "$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max not present (yet), nothing to copy", 2 ); } } # accepts src, dest # prints out the cp -al command that would be run, based on config file data sub display_cp_al { my $src = shift(@_); my $dest = shift(@_); # remove trailing slashes (for newer versions of GNU cp) $src = remove_trailing_slash($src); $dest = remove_trailing_slash($dest); if (!defined($src)) { bail('src not defined in display_cp_al()'); } if (!defined($dest)) { bail('dest not defined in display_cp_al()'); } if (defined($config_vars{'cmd_cp'})) { print_cmd("$config_vars{'cmd_cp'} -al $src $dest"); } else { print_cmd("native_cp_al(\"$src\", \"$dest\")"); } } # stub subroutine # calls either gnu_cp_al() or native_cp_al() # returns the value directly from whichever subroutine it calls # also prints out what's happening to the screen, if appropriate sub cp_al { my $src = shift(@_); my $dest = shift(@_); my $result = 0; # use gnu cp if we have it if (defined($config_vars{'cmd_cp'})) { $result = gnu_cp_al("$src", "$dest"); } # fall back to the built-in native perl replacement, followed by an rsync clean-up step else { # native cp -al $result = native_cp_al("$src", "$dest"); if (1 != $result) { return ($result); } # rsync clean-up $result = rsync_cleanup_after_native_cp_al("$src", "$dest"); } return ($result); } # This is to test whether cp -al seems to work in a simple case # return 0 if cp -al succeeds # return 1 if cp -al fails # return -1 if something else failed - test inconclusive sub test_cp_al { my $s = "$config_vars{'snapshot_root'}/cp_al1"; my $d = "$config_vars{'snapshot_root'}/cp_al2"; my $result; -d $s || mkdir($s) || return (-1); open(TT1, ">>$s/tt1") || return (-1); close(TT1) || return (-1); $result = system($config_vars{'cmd_cp'}, '-al', "$s", "$d"); if ($result != 0) { return (1); } unlink("$d/tt1"); unlink("$s/tt1"); rmdir($d); rmdir($s); return (0); } # this is a wrapper to call the GNU version of "cp" # it might fail in mysterious ways if you have a different version of "cp" # sub gnu_cp_al { my $src = shift(@_); my $dest = shift(@_); my $result = 0; my $status; # make sure we were passed two arguments if (!defined($src)) { return (0); } if (!defined($dest)) { return (0); } # remove trailing slashes (for newer versions of GNU cp) $src = remove_trailing_slash($src); $dest = remove_trailing_slash($dest); if (!-d "$src") { print_err("gnu_cp_al() needs a valid directory as an argument", 2); return (0); } # make the system call to GNU cp $result = system($config_vars{'cmd_cp'}, '-al', "$src", "$dest"); if ($result != 0) { $status = $result >> 8; print_err("$config_vars{'cmd_cp'} -al $src $dest failed (result $result, exit status $status).", 2); if (test_cp_al() > 0) { print_err("Perhaps your cp does not support -al options?", 2); } return (0); } return (1); } # This is a purpose built, native perl replacement for GNU "cp -al". # However, it is not quite as good. it does not copy "special" files: # block, char, fifo, or sockets. # Never the less, it does do regular files, directories, and symlinks # which should be enough for 95% of the normal cases. # If you absolutely have to have snapshots of FIFOs, etc, just get GNU # cp on your system, and specify it in the config file. # # Please note that more recently, this subroutine is followed up by # an rsync clean-up step. This combination effectively removes most of # the limitations of this technique. # # In the great perl tradition, this returns 1 on success, 0 on failure. # sub native_cp_al { my $src = shift(@_); my $dest = shift(@_); my $dh = undef; my $result = 0; # make sure we were passed two arguments if (!defined($src)) { return (0); } if (!defined($dest)) { return (0); } # make sure we have a source directory if (!-d "$src") { print_err("native_cp_al() needs a valid source directory as an argument", 2); return (0); } # strip trailing slashes off the directories, # since we'll add them back on later $src = remove_trailing_slash($src); $dest = remove_trailing_slash($dest); # LSTAT SRC my $st = lstat("$src"); if (!defined($st)) { print_err("Warning! Could not lstat source dir (\"$src\") : $!", 2); return (0); } # MKDIR DEST (AND SET MODE) if (!-d "$dest") { # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "mkdir(\"$dest\", " . get_perms($st->mode) . ")"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = mkdir("$dest", $st->mode); if (!$result) { print_err("Warning! Could not mkdir(\"$dest\", $st->mode) : $!", 2); return (0); } } # CHOWN DEST (if root) if (0 == $<) { # make sure destination is not a symlink if (!-l "$dest") { # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\")"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = safe_chown($st->uid, $st->gid, "$dest"); if (!$result) { print_err("Warning! Could not safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\");", 2); return (0); } } } # READ DIR CONTENTS $dh = new DirHandle("$src"); if (defined($dh)) { my @nodes = $dh->read(); # loop through all nodes in this dir foreach my $node (@nodes) { # skip '.' and '..' next if ($node =~ m/^\.\.?$/o); # make sure the node we just got is valid (this is highly unlikely to fail) my $st = lstat("$src/$node"); if (!defined($st)) { print_err("Warning! Could not lstat source node (\"$src/$node\") : $!", 2); next; } # SYMLINK (must be tested for first, because it will also pass the file and dir tests) if (-l "$src/$node") { # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "copy_symlink(\"$src/$node\", \"$dest/$node\")"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = copy_symlink("$src/$node", "$dest/$node"); if (0 == $result) { print_err("Warning! copy_symlink(\"$src/$node\", \"$dest/$node\")", 2); next; } # FILE } elsif (-f "$src/$node") { # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "link(\"$src/$node\", \"$dest/$node\");"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } # make a hard link $result = link("$src/$node", "$dest/$node"); if (!$result) { print_err("Warning! Could not link(\"$src/$node\", \"$dest/$node\") : $!", 2); next; } # DIRECTORY } elsif (-d "$src/$node") { # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "native_cp_al(\"$src/$node\", \"$dest/$node\")"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } # call this subroutine recursively, to create the directory $result = native_cp_al("$src/$node", "$dest/$node"); if (!$result) { print_err("Warning! Recursion error in native_cp_al(\"$src/$node\", \"$dest/$node\")", 2); next; } } ## rsync_cleanup_after_native_cp_al() will take care of the files we can't handle here # ## FIFO #} elsif ( -p "$src/$node" ) { # # print_err("Warning! Ignoring FIFO $src/$node", 2); # ## SOCKET #} elsif ( -S "$src/$node" ) { # # print_err("Warning! Ignoring socket: $src/$node", 2); # ## BLOCK DEVICE #} elsif ( -b "$src/$node" ) { # # print_err("Warning! Ignoring special block file: $src/$node", 2); # ## CHAR DEVICE #} elsif ( -c "$src/$node" ) { # # print_err("Warning! Ignoring special character file: $src/$node", 2); #} } } else { print_err("Could not open \"$src\". Do you have adequate permissions?", 2); return (0); } # close open dir handle if (defined($dh)) { $dh->close(); } undef($dh); # UTIME DEST # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "utime(" . $st->atime . ", " . $st->mtime . ", \"$dest\");"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = utime($st->atime, $st->mtime, "$dest"); if (!$result) { print_err("Warning! Could not set utime(" . $st->atime . ", " . $st->mtime . ", \"$dest\") : $!", 2); return (0); } return (1); } # If we're using native_cp_al(), it can't transfer special files. # So, to make sure no one misses out, this subroutine gets called every time directly # after native_cp_al(), with the same source and destinations paths. # # Essentially it is running between two almost identical hard linked directory trees. # However, it will transfer over the few (if any) special files that were otherwise # missed. # # This subroutine specifies its own parameters for rsync's arguments. This is to make # sure that nothing goes wrong, since there is not much here that should be left to # interpretation. # sub rsync_cleanup_after_native_cp_al { my $src = shift(@_); my $dest = shift(@_); my $local_rsync_short_args = '-a'; # if the user asked for -E, we should use it here too. # should we check for OS X? Dunno, but for now that extra # check is in here as we know we need it there, and so # this is the smallest change for the smallest number of # people $local_rsync_short_args .= 'E' if ( defined($config_vars{'rsync_short_args'}) && $config_vars{'rsync_short_args'} =~ /E/ && $^O eq 'darwin'); my @cmd_stack = (); # make sure we were passed two arguments if (!defined($src)) { return (0); } if (!defined($dest)) { return (0); } # make sure we have a source directory if (!-d "$src") { print_err("rsync_cleanup_after_native_cp_al() needs a valid source directory as an argument", 2); return (0); } # make sure we have a destination directory if (!-d "$dest") { print_err("rsync_cleanup_after_native_cp_al() needs a valid destination directory as an argument", 2); return (0); } # make sure src and dest both have a trailing slash for rsync $src =~ s/\/?$/\//; $dest =~ s/\/?$/\//; # check verbose settings and modify rsync's short args accordingly if ($verbose > 3) { $local_rsync_short_args .= 'v'; } # setup rsync command # # rsync push(@cmd_stack, $config_vars{'cmd_rsync'}); # # short args push(@cmd_stack, $local_rsync_short_args); # # long args (not the defaults) push(@cmd_stack, '--delete'); push(@cmd_stack, '--numeric-ids'); # # src push(@cmd_stack, "$src"); # # dest push(@cmd_stack, "$dest"); print_cmd(@cmd_stack); if (0 == $test) { my $result = system(@cmd_stack); if ($result != 0) { # bitmask return value my $retval = get_retval($result); # a partial list of rsync exit values # 0 Success # 23 Partial transfer due to error # 24 Partial transfer due to vanished source files if (23 == $retval) { print_warn( "Some files and/or directories in $src only transferred partially during rsync_cleanup_after_native_cp_al operation", 2 ); syslog_warn( "Some files and/or directories in $src only transferred partially during rsync_cleanup_after_native_cp_al operation" ); } elsif (24 == $retval) { print_warn( "Some files and/or directories in $src vanished during rsync_cleanup_after_native_cp_al operation", 2 ); syslog_warn( "Some files and/or directories in $src vanished during rsync_cleanup_after_native_cp_al operation"); } else { # other error bail("rsync returned error $retval in rsync_cleanup_after_native_cp_al()"); } } } return (1); } # accepts a path # displays the rm command according to the config file sub display_rm_rf { my $path = shift(@_); if (!defined($path)) { bail('display_rm_rf() requires an argument'); } if (defined($config_vars{'cmd_rm'})) { print_cmd("$config_vars{'cmd_rm'} -rf $path"); } else { print_cmd("rm -rf $path"); } } # stub subroutine # calls either cmd_rm_rf() or the native perl rmtree() # returns 1 on success, 0 on failure sub rm_rf { my $path = shift(@_); my $result = 0; # make sure we were passed an argument if (!defined($path)) { return (0); } # extra bonus safety feature! # confirm that whatever we're deleting must be inside the snapshot_root if (index($path, $config_vars{'snapshot_root'}) != 0) { bail("rm_rf() tried to delete something outside of $config_vars{'snapshot_root'}! Quitting now!"); } # use the rm command if we have it if (defined($config_vars{'cmd_rm'})) { $result = cmd_rm_rf("$path"); } # fall back on rmtree() else { # remove trailing slash just in case $path =~ s/\/$//; $result = rmtree("$path", 0, 0); } return ($result); } # this is a wrapper to the "rm" program, called with the "-rf" flags. sub cmd_rm_rf { my $path = shift(@_); my $result = 0; # make sure we were passed an argument if (!defined($path)) { return (0); } if (!-e "$path") { print_err("cmd_rm_rf() needs a valid file path as an argument", 2); return (0); } # make the system call to /bin/rm $result = system($config_vars{'cmd_rm'}, '-rf', "$path"); if ($result != 0) { print_err("Warning! $config_vars{'cmd_rm'} failed.", 2); return (0); } return (1); } # accepts no arguments # calls the 'du' command to show rsnapshot's disk usage # exits the program with 0 for success, 1 for failure # # this subroutine isn't like a lot of the "real" ones that write to logfiles, etc. # that's why the print_* subroutines aren't used here. # sub show_disk_usage { my @du_dirs = (); my $cmd_du = 'du'; my $du_args = $default_du_args; my $dest_path = ''; my $retval; # first, make sure we have permission to see the snapshot root if (!-r "$config_vars{'snapshot_root'}") { print STDERR ("ERROR: Permission denied\n"); exit(1); } # check for 'du' program if (defined($config_vars{'cmd_du'})) { # it was specified in the config file, use that version $cmd_du = $config_vars{'cmd_du'}; } # check for du args if (defined($config_vars{'du_args'})) { # it this was specified in the config file, use that version $du_args = $config_vars{'du_args'}; } # are we looking in subdirectories or at files? if (defined($ARGV[1])) { $dest_path = $ARGV[1]; # consolidate multiple slashes $dest_path =~ s/\/+/\//o; if (is_directory_traversal($dest_path)) { print STDERR "ERROR: Directory traversal is not allowed\n"; exit(1); } if (!is_valid_local_non_abs_path($dest_path)) { print STDERR "ERROR: Full paths are not allowed\n"; exit(1); } } # find the directories to look through, in order # only add them to the list if we have read permissions if (-r "$config_vars{'snapshot_root'}/") { # if we have a .sync directory, that will have the most recent files, and should be first if (-d "$config_vars{'snapshot_root'}/.sync") { if (-r "$config_vars{'snapshot_root'}/.sync") { push(@du_dirs, "$config_vars{'snapshot_root'}/.sync"); } } # loop through the intervals, most recent to oldest foreach my $interval_ref (@intervals) { my $interval = $$interval_ref{'interval'}; my $max_interval_num = $$interval_ref{'number'}; for (my $i = 0; $i < $max_interval_num; $i++) { if (-r "$config_vars{'snapshot_root'}/$interval.$i/$dest_path") { push(@du_dirs, "$config_vars{'snapshot_root'}/$interval.$i/$dest_path"); } } } } # if we can see any of the intervals, find out how much space they're taking up # most likely we can either see all of them or none at all if (scalar(@du_dirs) > 0) { my @cmd_stack = ($cmd_du, split_long_args_with_quotes('du_args', $du_args), @du_dirs); if (defined($verbose) && ($verbose >= 3)) { print wrap_cmd(join(' ', @cmd_stack)), "\n\n"; } if (0 == $test) { $retval = system(@cmd_stack); if (0 == $retval) { # exit showing success exit(0); } else { # exit showing error print STDERR "Error while calling $cmd_du.\n"; print STDERR "Please make sure this version of du supports the \"$du_args\" flags.\n"; print STDERR "GNU du is recommended.\n"; exit(1); } } else { # test was successful exit(0); } } else { print STDERR ("No files or directories found\n"); exit(1); } # shouldn't happen exit(1); } # accept two args from $ARGV[1] and [2], like "beta.0" "beta.1" etc. # stick the full snapshot_root path on the beginning, and call rsnapshot-diff with these args # NOTE: since this is a read-only operation, we're not concerned with directory traversals and relative paths sub show_rsnapshot_diff { my $cmd_rsnapshot_diff = 'rsnapshot-diff'; my $retval; # this will only hold two entries, no more no less # paths_in holds the incoming arguments # args will be assigned the arguments that rsnapshot-diff will use # my @paths_in = (); my @cmd_args = (); # first, make sure we have permission to see the snapshot root if (!-r "$config_vars{'snapshot_root'}") { print STDERR ("ERROR: Permission denied\n"); exit(1); } # check for rsnapshot-diff program (falling back on $PATH) if (defined($config_vars{'cmd_rsnapshot_diff'})) { $cmd_rsnapshot_diff = $config_vars{'cmd_rsnapshot_diff'}; } # see if we even got the right number of arguments (none is OK, but 1 isn't. 2 is also OK) if (defined($ARGV[1]) && !defined($ARGV[2])) { print STDERR "Usage: rsnapshot diff [backup level|dir] [backup level|dir]\n"; exit(1); } # make this automatically pick the two lowest intervals (or .sync dir) for comparison, as the default # we actually want to specify the older directory first, since rsnapshot-diff will flip them around # anyway based on mod times. doing it this way should make both programs consistent, and cause less # surprises. if (!defined($ARGV[1]) && !defined($ARGV[2])) { # sync_first is enabled, and .sync exists if ($config_vars{'sync_first'} && (-d "$config_vars{'snapshot_root'}/.sync/")) { # interval.0 if (-d ("$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0")) { $cmd_args[0] = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0"; } # .sync $cmd_args[1] = "$config_vars{'snapshot_root'}/.sync"; } # sync_first is not enabled, or .sync doesn't exist else { # interval.1 if (-d ("$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".1")) { $cmd_args[0] = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".1"; } # interval.0 if (-d ("$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0")) { $cmd_args[1] = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0"; } } } # if we got some command line arguments, loop through twice and figure out what they mean else { $paths_in[0] = $ARGV[1]; # the 1st path is the 2nd cmd line argument $paths_in[1] = $ARGV[2]; # the 2nd path is the 3rd cmd line argument for (my $i = 0; $i < 2; $i++) { # no interval would start with ../ if (is_directory_traversal("$paths_in[$i]")) { $cmd_args[$i] = $paths_in[$i]; # if this directory exists locally, it must be local } elsif (-e "$paths_in[$i]") { $cmd_args[$i] = $paths_in[$i]; # absolute path } elsif (is_valid_local_abs_path("$paths_in[$i]")) { $cmd_args[$i] = $paths_in[$i]; # we didn't find it locally, but it's in the snapshot root } elsif (-e "$config_vars{'snapshot_root'}/$paths_in[$i]") { $cmd_args[$i] = "$config_vars{'snapshot_root'}/$paths_in[$i]"; } } } # double check to make sure the directories exists (and are directories) if ( (!defined($cmd_args[0]) or (!defined($cmd_args[1]))) or ((!-d "$cmd_args[0]") or (!-d "$cmd_args[1]"))) { print STDERR "ERROR: Arguments must be valid backup levels or directories\n"; exit(1); } # remove trailing slashes from directories $cmd_args[0] = remove_trailing_slash($cmd_args[0]); $cmd_args[1] = remove_trailing_slash($cmd_args[1]); # increase verbosity (by possibly sticking a verbose flag in as the first argument) # # debug if ($verbose >= 5) { unshift(@cmd_args, '-V'); } elsif ($verbose >= 4) { unshift(@cmd_args, '-v'); # verbose } elsif ($verbose >= 3) { unshift(@cmd_args, '-vi'); } # run rsnapshot-diff if (defined($verbose) && ($verbose >= 3)) { print wrap_cmd(("$cmd_rsnapshot_diff " . join(' ', @cmd_args))), "\n\n"; } if (0 == $test) { $retval = system($cmd_rsnapshot_diff, @cmd_args); if (0 == $retval) { exit(0); } else { # exit showing error print STDERR "Error while calling $cmd_rsnapshot_diff\n"; exit(1); } } else { # test was successful exit(0); } # shouldn't happen exit(1); } # This subroutine works the way I hoped rsync would under certain conditions. # This is no fault of rsync, I just had something slightly different in mind :) # # This subroutine accepts two arguments, a source path and a destination path. # It traverses both recursively. # If a file is in the source, but not the destination, it is hard linked into dest # If a file is in the destination, but not the source, it is deleted # If a file is in both locations and is different, dest is unlinked and src is linked to dest # If a file is in both locations and is the same, nothing happens # # What makes this different than rsync is that it looks only at the file contents to # see if the files are different, not at the metadata such as timestamps. # I was unable to make rsync work recursively on identical files without unlinking # at the destination and using another inode for a new file with the exact same content. # # If anyone knows of a better way (that doesn't add dependencies) i'd love to hear it! # sub sync_if_different { my $src = shift(@_); my $dest = shift(@_); my $result = 0; # make sure we were passed two arguments if (!defined($src)) { return (0); } if (!defined($dest)) { return (0); } # make sure we have a source directory if (!-d "$src") { print_err("sync_if_different() needs a valid source directory as its first argument", 2); return (0); } # strip trailing slashes off the directories, # since we'll add them back on later $src = remove_trailing_slash($src); $dest = remove_trailing_slash($dest); # copy everything from src to dest # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "sync_cp_src_dest(\"$src\", \"$dest\")"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = sync_cp_src_dest("$src", "$dest"); if (!$result) { print_err("Warning! sync_cp_src_dest(\"$src\", \"$dest\")", 2); return (0); } # delete everything from dest that isn't in src # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "sync_rm_dest(\"$src\", \"$dest\")"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = sync_rm_dest("$src", "$dest"); if (!$result) { print_err("Warning! sync_rm_dest(\"$src\", \"$dest\")", 2); return (0); } return (1); } # accepts src, dest # "copies" everything from src to dest, mainly using hard links # called only from sync_if_different() # returns 1 on success, 0 if any failures occur sub sync_cp_src_dest { my $src = shift(@_); my $dest = shift(@_); my $dh = undef; my $result = 0; my $retval = 1; # return code for this subroutine # make sure we were passed two arguments if (!defined($src)) { return (0); } if (!defined($dest)) { return (0); } # make sure we have a source directory if (!-d "$src") { print_err("sync_if_different() needs a valid source directory as its first argument", 2); return (0); } # strip trailing slashes off the directories, # since we'll add them back on later $src = remove_trailing_slash($src); $dest = remove_trailing_slash($dest); # LSTAT SRC my $st = lstat("$src"); if (!defined($st)) { print_err("Could not lstat(\"$src\")", 2); return (0); } # MKDIR DEST (AND SET MODE) if (!-d "$dest") { # check to make sure we don't have something here that's not a directory if (-e "$dest") { $result = unlink("$dest"); if (0 == $result) { print_err("Warning! Could not unlink(\"$dest\")", 2); return (0); } } # create the directory $result = mkdir("$dest", $st->mode); if (!$result) { print_err("Warning! Could not mkdir(\"$dest\", $st->mode);", 2); return (0); } } # CHOWN DEST (if root) if (0 == $<) { # make sure destination is not a symlink (should never happen because of unlink() above) if (!-l "$dest") { $result = safe_chown($st->uid, $st->gid, "$dest"); if (!$result) { print_err("Warning! Could not safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\");", 2); return (0); } } } # copy anything different from src into dest $dh = new DirHandle("$src"); if (defined($dh)) { my @nodes = $dh->read(); # loop through all nodes in this dir foreach my $node (@nodes) { # skip '.' and '..' next if ($node =~ m/^\.\.?$/o); # if it's a symlink, create the link # this check must be done before dir and file because it will # pretend to be a file or a directory as well as a symlink if (-l "$src/$node") { # nuke whatever is in the destination, since we'd have to recreate the symlink anyway # and a real file or directory will be in our way # symlinks pretend to be directories, which is why we check it the way that we do if (-e "$dest/$node") { if ((-l "$dest/$node") or (!-d "$dest/$node")) { $result = unlink("$dest/$node"); if (0 == $result) { print_err("Warning! Could not unlink(\"$dest/$node\")", 2); next; } } # nuke the destination directory else { $result = rm_rf("$dest/$node"); if (0 == $result) { print_err("Could not rm_rf(\"$dest/$node\")", 2); next; } } } $result = copy_symlink("$src/$node", "$dest/$node"); if (0 == $result) { print_err("Warning! copy_symlink(\"$src/$node\", \"$dest/$node\") failed", 2); return (0); } } # if it's a directory, recurse! elsif (-d "$src/$node") { # if the destination exists but isn't a directory, delete it if (-e "$dest/$node") { # a symlink might claim to be a directory, so check for that first if ((-l "$dest/$node") or (!-d "$dest/$node")) { $result = unlink("$dest/$node"); if (0 == $result) { print_err("Warning! unlink(\"$dest/$node\") failed", 2); next; } } } # ok, dest is a real directory or it isn't there yet, go recurse $result = sync_cp_src_dest("$src/$node", "$dest/$node"); if (!$result) { print_err("Warning! Recursion error in sync_cp_src_dest(\"$src/$node\", \"$dest/$node\")", 2); } } # if it's a file... elsif (-f "$src/$node") { # if dest is a symlink, we need to remove it first if (-l "$dest/$node") { $result = unlink("$dest/$node"); if (0 == $result) { print_err("Warning! unlink(\"$dest/$node\") failed", 2); next; } } # if dest is a directory, we need to wipe it out first if (-d "$dest/$node") { $result = rm_rf("$dest/$node"); if (0 == $result) { print_err("Could not rm_rf(\"$dest/$node\")", 2); return (0); } } # if dest (still) exists, check for differences if (-e "$dest/$node") { # if they are different, unlink dest and link src to dest if (1 == file_diff("$src/$node", "$dest/$node")) { $result = unlink("$dest/$node"); if (0 == $result) { print_err("Warning! unlink(\"$dest/$node\") failed", 2); next; } $result = link("$src/$node", "$dest/$node"); if (0 == $result) { print_err("Warning! link(\"$src/$node\", \"$dest/$node\") failed", 2); next; } } # if they are the same, just leave dest alone else { next; } } # ok, dest doesn't exist. just link src to dest else { $result = link("$src/$node", "$dest/$node"); if (0 == $result) { print_err("Warning! link(\"$src/$node\", \"$dest/$node\") failed", 2); } } } # FIFO elsif (-p "$src/$node") { print_err("Warning! Ignoring FIFO $src/$node", 2); } # SOCKET elsif (-S "$src/$node") { print_err("Warning! Ignoring socket: $src/$node", 2); } # BLOCK DEVICE elsif (-b "$src/$node") { print_err("Warning! Ignoring special block file: $src/$node", 2); } # CHAR DEVICE elsif (-c "$src/$node") { print_err("Warning! Ignoring special character file: $src/$node", 2); } } } # close open dir handle if (defined($dh)) { $dh->close(); } undef($dh); return (1); } # accepts src, dest # deletes everything from dest that isn't in src also # called only from sync_if_different() sub sync_rm_dest { my $src = shift(@_); my $dest = shift(@_); my $dh = undef; my $result = 0; # make sure we were passed two arguments if (!defined($src)) { return (0); } if (!defined($dest)) { return (0); } # make sure we have a source directory if (!-d "$src") { print_err("sync_rm_dest() needs a valid source directory as its first argument", 2); return (0); } # make sure we have a destination directory if (!-d "$dest") { print_err("sync_rm_dest() needs a valid destination directory as its second argument", 2); return (0); } # strip trailing slashes off the directories, # since we'll add them back on later $src = remove_trailing_slash($src); $dest = remove_trailing_slash($dest); # delete anything from dest that isn't found in src $dh = new DirHandle("$dest"); if (defined($dh)) { my @nodes = $dh->read(); # loop through all nodes in this dir foreach my $node (@nodes) { # skip '.' and '..' next if ($node =~ m/^\.\.?$/o); # if this node isn't present in src, delete it if (!-e "$src/$node") { # file or symlink if ((-l "$dest/$node") or (!-d "$dest/$node")) { $result = unlink("$dest/$node"); if (0 == $result) { print_err("Warning! Could not delete \"$dest/$node\"", 2); next; } } # directory else { $result = rm_rf("$dest/$node"); if (0 == $result) { print_err("Warning! Could not delete \"$dest/$node\"", 2); } } next; } # ok, this also exists in src... # theoretically, sync_cp_src_dest() should have caught this already, but better safe than sorry # also, symlinks can pretend to be directories, so we have to check for those too # if src is a file but dest is a directory, we need to recursively remove the dest dir if ((-l "$src/$node") or (!-d "$src/$node")) { if (-d "$dest/$node") { $result = rm_rf("$dest/$node"); if (0 == $result) { print_err("Warning! Could not delete \"$dest/$node\"", 2); } } # otherwise, if src is a directory, but dest is a file, remove the file in dest } elsif (-d "$src/$node") { if ((-l "$dest/$node") or (!-d "$dest/$node")) { $result = unlink("$dest/$node"); if (0 == $result) { print_err("Warning! Could not delete \"$dest/$node\"", 2); next; } } } # if it's a directory in src, let's recurse into it and compare files there if (-d "$src/$node") { $result = sync_rm_dest("$src/$node", "$dest/$node"); if (!$result) { print_err("Warning! Recursion error in sync_rm_dest(\"$src/$node\", \"$dest/$node\")", 2); } } } } # close open dir handle if (defined($dh)) { $dh->close(); } undef($dh); return (1); } # accepts src, dest # "copies" a symlink from src by recreating it in dest # returns 1 on success, 0 on failure sub copy_symlink { my $src = shift(@_); my $dest = shift(@_); my $st = undef; my $result = undef; my $link_deref_path = undef; # make sure it's actually a symlink if (!-l "$src") { print_err("Warning! \"$src\" not a symlink in copy_symlink()", 2); return (0); } # make sure we aren't clobbering the destination if (-e "$dest") { print_err("Warning! \"$dest\" exists!", 2); return (0); } # LSTAT $st = lstat("$src"); if (!defined($st)) { print_err("Warning! lstat(\"$src\") failed", 2); return (0); } # CREATE THE SYMLINK # This is done in two steps: # Reading/dereferencing the link, and creating a new one # # Why not just hard link the symlink? # see http://www.rsnapshot.org/security/2005/001.html # and also msgid <5036B23B.3000606@scubaninja.com> on # rsnapshot-discuss, on 2012-08-23 # # Step 1: READ THE LINK if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "readlink(\"$src\")\n"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $link_deref_path = readlink("$src"); if (!defined($link_deref_path)) { print_err("Warning! Could not readlink(\"$src\")", 2); return (0); } # # Step 2: RECREATE THE LINK if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "symlink(\"$link_deref_path\", \"$dest\")\n"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = symlink("$link_deref_path", "$dest"); if (0 == $result) { print_err("Warning! Could not symlink(\"$link_deref_path\"), \"$dest\")", 2); return (0); } # CHOWN DEST (if root) if (0 == $<) { # make sure the symlink even exists if (-e "$dest") { # print and/or log this if necessary if (($verbose > 4) or ($loglevel > 4)) { my $cmd_string = "safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\");"; if ($verbose > 4) { print_cmd($cmd_string); } elsif ($loglevel > 4) { log_msg($cmd_string, 4); } } $result = safe_chown($st->uid, $st->gid, "$dest"); if (0 == $result) { print_err("Warning! Could not safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\")", 2); return (0); } } } return (1); } # accepts a file permission number from $st->mode (e.g., 33188) # returns a "normal" file permission number (e.g., 644) # do the appropriate bit shifting to get a "normal" UNIX file permission mode sub get_perms { my $raw_mode = shift(@_); if (!defined($raw_mode)) { return (undef); } # a lot of voodoo for just one line # http://www.perlmonks.org/index.pl?node_id=159906 my $mode = sprintf("%04o", ($raw_mode & 07777)); return ($mode); } # accepts return value from the system() command # bitmasks it, and returns the same thing "echo $?" would from the shell sub get_retval { my $retval = shift(@_); if (!defined($retval)) { bail('get_retval() was not passed a value'); } if ($retval !~ m/^\d+$/) { bail("get_retval() was passed $retval, a number is required"); } return ($retval / 256); } # accepts two file paths # returns 0 if they're the same, 1 if they're different # returns undef if one or both of the files can't be found, opened, or closed sub file_diff { my $file1 = shift(@_); my $file2 = shift(@_); my $st1 = undef; my $st2 = undef; my $buf1 = undef; my $buf2 = undef; my $result = undef; # number of bytes to read at once my $BUFSIZE = 16384; # boolean file comparison flag. assume they're the same. my $is_different = 0; if (!-r "$file1") { return (undef); } if (!-r "$file2") { return (undef); } # CHECK FILE SIZES FIRST $st1 = lstat("$file1"); $st2 = lstat("$file2"); if (!defined($st1)) { return (undef); } if (!defined($st2)) { return (undef); } # if the files aren't even the same size, they can't possibly be the same. # don't waste time comparing them more intensively if ($st1->size != $st2->size) { return (1); } # ok, we're still here. # that means we have to compare files one chunk at a time # open both files $result = open(FILE1, "$file1"); if (!defined($result)) { return (undef); } $result = open(FILE2, "$file2"); if (!defined($result)) { close(FILE1); return (undef); } # compare files while (read(FILE1, $buf1, $BUFSIZE) && read(FILE2, $buf2, $BUFSIZE)) { # exit this loop as soon as possible if ($buf1 ne $buf2) { $is_different = 1; last; } } # close both files $result = close(FILE2); if (!defined($result)) { close(FILE1); return (undef); } $result = close(FILE1); if (!defined($result)) { return (undef); } # return our findings return ($is_different); } # accepts src, dest (file paths) # calls rename(), forcing the mtime to be correct (to work around a bug in rare versions of the Linux 2.4 kernel) # returns 1 on success, 0 on failure, just like the real rename() command sub safe_rename { my $src = shift(@_); my $dest = shift(@_); my $st; my $retval; my $result; # validate src and dest paths if (!defined($src)) { print_err("safe_rename() needs a valid source file path as an argument", 2); return (0); } if (!defined($dest)) { print_err("safe_rename() needs a valid destination file path as an argument", 2); return (0); } # stat file before rename $st = stat($src); if (!defined($st)) { print_err("Could not stat() \"$src\"", 2); return (0); } # rename the file $retval = rename("$src", "$dest"); if (1 != $retval) { print_err("Could not rename(\"$src\", \"$dest\")", 2); return (0); } # give it back the old mtime and atime values $result = utime($st->atime, $st->mtime, "$dest"); if (!defined($result)) { print_err("Could not utime( $st->atime, $st->mtime, \"$dest\")", 2); return (0); } # if we made it this far, it must have worked return (1); } # accepts no args # checks the config file for version number # prints the config version to stdout # exits the program, 0 on success, 1 on failure # this feature is "undocumented", for use with scripts, etc sub check_config_version { my $version = get_config_version(); if (!defined($version)) { print "error\n"; exit(1); } print $version, "\n"; exit(0); } # accepts no args # scans the config file for the config_version parameter # returns the config version, or undef sub get_config_version { my $result; my $version; # make sure the config file exists and we can read it if (!defined($config_file)) { return (undef); } if (!-r "$config_file") { return (undef); } # open the config file $result = open(CONFIG, "$config_file"); if (!defined($result)) { return (undef); } # scan the config file looking for the config_version parameter # if we find it, exit the loop while (my $line = ) { chomp($line); if ($line =~ m/^config_version/o) { if ($line =~ m/^config_version\t+([\d\.\-\w]+)$/o) { $version = $1; last; } else { $version = 'undefined'; } } } $result = close(CONFIG); if (!defined($result)) { return (undef); } if (!defined($version)) { $version = 'unknown'; } return ($version); } # accepts no args # exits the program, 0 on success, 1 on failure # attempts to upgrade the rsnapshot.conf file for compatibility with this version sub upgrade_config_file { my $result; my @lines; my $config_version; # check if rsync_long_args is already enabled my $rsync_long_args_enabled = 0; # first, see if the file isn't already up to date $config_version = get_config_version(); if (!defined($config_version)) { print STDERR "ERROR: Could not read config file during version check.\n"; exit(1); } # right now 1.2 is the only valid version if ('1.2' eq $config_version) { print "$config_file file is already up to date.\n"; exit(0); # config_version is set, but not to anything we know about } elsif ('unknown' eq $config_version) { # this is good, it means the config_version was not already set to anything # and is a good candidate for the upgrade } else { print STDERR "ERROR: config_version is set to unknown version: $config_version.\n"; exit(1); } # make sure config file is present and readable if (!defined($config_file)) { print STDERR "ERROR: Config file not defined.\n"; exit(1); } if (!-r "$config_file") { print STDERR "ERROR: $config_file not readable.\n"; exit(1); } # read in original config file $result = open(CONFIG, "$config_file"); if (!defined($result)) { print STDERR "ERROR: Could not open $config_file for reading.\n"; exit(1); } @lines = ; $result = close(CONFIG); if (!defined($result)) { print STDERR "ERROR: Could not close $config_file after reading.\n"; exit(1); } # see if we can find rsync_long_args, either commented out or uncommented foreach my $line (@lines) { if ($line =~ m/^rsync_long_args/o) { $rsync_long_args_enabled = 1; } } # back up old config file backup_config_file(\@lines); # found rsync_long_args enabled if ($rsync_long_args_enabled) { print "Found \"rsync_long_args\" uncommented. Attempting upgrade...\n"; write_upgraded_config_file(\@lines, 0); } # did not find rsync_long_args enabled else { print "Could not find old \"rsync_long_args\" parameter. Attempting upgrade...\n"; write_upgraded_config_file(\@lines, 1); } print "\"$config_file\" was successfully upgraded.\n"; exit(0); } # accepts array_ref of config file lines # exits 1 on errors # attempts to backup rsnapshot.conf to rsnapshot.conf.backup.(#) sub backup_config_file { my $lines_ref = shift(@_); my $result; my $backup_config_file; my $backup_exists = 0; if (!defined($lines_ref)) { print STDERR "ERROR: backup_config_file() was not passed an argument.\n"; exit(1); } if (!defined($config_file)) { print STDERR "ERROR: Could not find config file.\n"; exit(1); } $backup_config_file = "$config_file.backup"; print "Backing up \"$config_file\".\n"; # pick a unique name for the backup file if (-e "$backup_config_file") { $backup_exists = 1; for (my $i = 0; $i < 100; $i++) { if (!-e "$backup_config_file.$i") { $backup_config_file = "$backup_config_file.$i"; $backup_exists = 0; last; } } # if we couldn't write a backup file, exit with an error if (1 == $backup_exists) { print STDERR "ERROR: Refusing to overwrite $backup_config_file.\n"; print STDERR "Please move $backup_config_file out of the way and try again.\n"; print STDERR "$config_file has NOT been upgraded!\n"; exit(1); } } $result = open(OUTFILE, "> $backup_config_file"); if (!defined($result) or ($result != 1)) { print STDERR "Error opening $backup_config_file for writing.\n"; print STDERR "$config_file has NOT been upgraded!\n"; exit(1); } foreach my $line (@$lines_ref) { print OUTFILE $line; } $result = close(OUTFILE); if (!defined($result) or (1 != $result)) { print STDERR "could not cleanly close $backup_config_file.\n"; print STDERR "$config_file has NOT been upgraded!\n"; exit(1); } print "Config file was backed up to \"$backup_config_file\".\n"; } # accepts no args # exits 1 on errors # attempts to write an upgraded config file to rsnapshot.conf sub write_upgraded_config_file { my $lines_ref = shift(@_); my $add_rsync_long_args = shift(@_); my $result; my $upgrade_notice = ''; $upgrade_notice .= "#-----------------------------------------------------------------------------\n"; $upgrade_notice .= "# UPGRADE NOTICE:\n"; $upgrade_notice .= "#\n"; $upgrade_notice .= "# This file was upgraded automatically by rsnapshot.\n"; $upgrade_notice .= "#\n"; $upgrade_notice .= "# The \"config_version\" parameter was added, since it is now required.\n"; $upgrade_notice .= "#\n"; $upgrade_notice .= "# The default value for \"rsync_long_args\" has changed in this release.\n"; $upgrade_notice .= "# By explicitly setting it to the old default values, rsnapshot will still\n"; $upgrade_notice .= "# behave like it did in previous versions.\n"; $upgrade_notice .= "#\n"; if (defined($add_rsync_long_args) && (1 == $add_rsync_long_args)) { $upgrade_notice .= "# In this file, \"rsync_long_args\" was not enabled before the upgrade,\n"; $upgrade_notice .= "# so it has been set to the old default value.\n"; } else { $upgrade_notice .= "# In this file, \"rsync_long_args\" was already enabled before the upgrade,\n"; $upgrade_notice .= "# so it was not changed.\n"; } $upgrade_notice .= "#\n"; $upgrade_notice .= "# New features and improvements have been added to rsnapshot that can\n"; $upgrade_notice .= "# only be fully utilized by making some additional changes to\n"; $upgrade_notice .= "# \"rsync_long_args\" and your \"backup\" points. If you would like to get the\n"; $upgrade_notice .= "# most out of rsnapshot, please read the INSTALL file that came with this\n"; $upgrade_notice .= "# program for more information.\n"; $upgrade_notice .= "#-----------------------------------------------------------------------------\n"; if (!defined($config_file)) { print STDERR "ERROR: Config file not found.\n"; exit(1); } if (!-w "$config_file") { print STDERR "ERROR: \"$config_file\" is not writable.\n"; exit(1); } $result = open(CONFIG, "> $config_file"); if (!defined($result)) { print "ERROR: Could not open \"$config_file\" for writing.\n"; exit(1); } print CONFIG $upgrade_notice; print CONFIG "\n"; print CONFIG "config_version\t1.2\n"; print CONFIG "\n"; if (defined($add_rsync_long_args) && (1 == $add_rsync_long_args)) { print CONFIG "rsync_long_args\t--delete --numeric-ids\n"; print CONFIG "\n"; } foreach my $line (@$lines_ref) { print CONFIG "$line"; } $result = close(CONFIG); if (!defined($result)) { print STDERR "ERROR: Could not close \"$config_file\" after writing\n."; exit(1); } } # accepts no arguments # dynamically loads the CPAN Lchown module, if available # sets the global variable $have_lchown sub use_lchown { if ($verbose >= 5) { print_msg('require Lchown', 5); } eval { require Lchown; }; if ($@) { $have_lchown = 0; if ($verbose >= 5) { print_msg('Lchown module not found', 5); } return (0); } # if it loaded, see if this OS supports the lchown() system call { no strict 'subs'; if (defined(Lchown) && defined(Lchown::LCHOWN_AVAILABLE)) { if (1 == Lchown::LCHOWN_AVAILABLE()) { $have_lchown = 1; if ($verbose >= 5) { print_msg('Lchown module loaded successfully', 5); } return (1); } } } if ($verbose >= 5) { print_msg("Lchown module loaded, but operating system doesn't support lchown()", 5); } return (0); } # accepts uid, gid, filepath # uses lchown() to change ownership of the file, if possible # returns 1 upon success (or if lchown() not present) # returns 0 on failure sub safe_chown { my $uid = shift(@_); my $gid = shift(@_); my $filepath = shift(@_); my $result = undef; if (!defined($uid) or !defined($gid) or !defined($filepath)) { print_err("safe_chown() needs uid, gid, and filepath", 2); return (0); } if (!-e "$filepath") { print_err("safe_chown() needs a valid filepath (not \"$filepath\")", 2); return (0); } # if it's a symlink, use lchown() or skip it if (-l "$filepath") { # use Lchown if (1 == $have_lchown) { $result = Lchown::lchown($uid, $gid, "$filepath"); if (!defined($result)) { return (0); } } # we can't safely do anything here, skip it else { raise_warning(); if ($verbose > 2) { print_warn("Could not lchown() symlink \"$filepath\"", 2); } elsif ($loglevel > 2) { log_warn("Could not lchown() symlink \"$filepath\"", 2); } # we'll still return 1 at the bottom, because we did as well as we could # the warning raised will tell the user what happened } } # if it's not a symlink, use chown() else { $result = chown($uid, $gid, "$filepath"); if (!$result) { return (0); } } return (1); } ######################################## ### PERLDOC / POD ### ######################################## =pod =head1 NAME rsnapshot - remote filesystem snapshot utility =head1 SYNOPSIS B [B<-vtxqVD>] [B<-c> cfgfile] [command] [args] =head1 DESCRIPTION B is a filesystem snapshot utility. It can take incremental snapshots of local and remote filesystems for any number of machines. Local filesystem snapshots are handled with B. Secure remote connections are handled with rsync over B, while anonymous rsync connections simply use an rsync server. Both remote and local transfers depend on rsync. B saves much more disk space than you might imagine. The amount of space required is roughly the size of one full backup, plus a copy of each additional file that is changed. B makes extensive use of hard links, so if the file doesn't change, the next snapshot is simply a hard link to the exact same file. B will typically be invoked as root by a cron job, or series of cron jobs. It is possible, however, to run as any arbitrary user with an alternate configuration file. All important options are specified in a configuration file, which is located by default at B. An alternate file can be specified on the command line. There are also additional options which can be passed on the command line. The command line options are as follows: =over 4 B<-v> verbose, show shell commands being executed B<-t> test, show shell commands that would be executed B<-c> path to alternate config file B<-x> one filesystem, don't cross partitions within each backup point B<-q> quiet, suppress non-fatal warnings B<-V> same as -v, but with more detail B<-D> a firehose of diagnostic information =back =head1 CONFIGURATION B is the default configuration file. All parameters in this file must be separated by tabs. B can be used as a reference. It is recommended that you copy B to B, and then modify B to suit your needs. Long lines may be split over several lines. "Continuation" lines B begin with a space or a tab character. Continuation lines will have all leading and trailing whitespace stripped off, and then be appended with an intervening tab character to the previous line when the configuration file is parsed. Here is a list of allowed parameters: =over 4 B Config file version (required). Default is 1.2 B Local filesystem path to save all snapshots B Include another file in the configuration at this point. =over 4 This is recursive, but you may need to be careful about paths when specifying which file to include. We check to see if the file you have specified is readable, and will yell an error if it isn't. We recommend using a full path. As a special case, include_conf's value may be enclosed in `backticks` in which case it will be executed and whatever it spits to STDOUT will be included in the configuration. Note that shell meta-characters may be interpreted. =back B If set to 1, rsnapshot won't create snapshot_root directory B Full path to rsync (required) B Full path to ssh (optional) B Full path to cp (optional, but must be GNU version) =over 4 If you are using Linux, you should uncomment cmd_cp. If you are using a platform which does not have GNU cp, you should leave cmd_cp commented out. With GNU cp, rsnapshot can take care of both normal files and special files (such as FIFOs, sockets, and block/character devices) in one pass. If cmd_cp is disabled, rsnapshot will use its own built-in function, native_cp_al() to backup up regular files and directories. This will then be followed up by a separate call to rsync, to move the special files over (assuming there are any). =back B Full path to rm (optional) B Full path to logger (optional, for syslog support) B Full path to du (optional, for disk usage reports) B Full path to rsnapshot-diff (optional) B =over 4 Full path (plus any arguments) to preexec script (optional). This script will run immediately before each backup operation (but not any rotations). If the execution fails, rsnapshot will stop immediately. =back B =over 4 Full path (plus any arguments) to postexec script (optional). This script will run immediately after each backup operation (but not any rotations). If the execution fails, rsnapshot will stop immediately. =back B B B B =over 4 Paths to lvcreate, lvremove, mount and umount commands, for use with Linux LVMs. You may include options to the commands also. The lvcreate, lvremove, mount and umount commands are required for managing snapshots of LVM volumes and are otherwise optional. =back B [name] [number] =over 4 "name" refers to the name of this backup level (e.g., alpha, beta, so also called the 'interval'). "number" is the number of snapshots for this type of interval that will be retained. The value of "name" will be the command passed to B to perform this type of backup. A deprecated alias for 'retain' is 'interval'. Example: B [root@localhost]# B For this example, every time this is run, the following will happen: /alpha.5/ will be deleted, if it exists. /alpha.{1,2,3,4} will all be rotated +1, if they exist. /alpha.0/ will be copied to /alpha.1/ using hard links. Each backup point (explained below) will then be rsynced to the corresponding directories in /alpha.0/ Backup levels must be specified in the config file in order, from most frequent to least frequent. The first entry is the one which will be synced with the backup points. The subsequent backup levels (e.g., beta, gamma, etc) simply rotate, with each higher backup level pulling from the one below it for its .0 directory. Example: =over 4 B B B =back beta.0/ will be moved from alpha.5/, and gamma.0/ will be moved from beta.6/ alpha.0/ will be rsynced directly from the filesystem. =back B =over 4 If your version of rsync supports --link-dest (2.5.7 or newer), you can enable this to let rsync handle some things that GNU cp or the built-in subroutines would otherwise do. Enabling this makes rsnapshot take a slightly more complicated code branch, but it's the best way to support special files on non-Linux systems. =back B =over 4 sync_first changes the behaviour of rsnapshot. When this is enabled, all calls to rsnapshot with various backup levels simply rotate files. All backups are handled by calling rsnapshot with the "sync" argument. The synced files are stored in a ".sync" directory under the snapshot_root. This allows better recovery in the event that rsnapshot is interrupted in the middle of a sync operation, since the sync step and rotation steps are separated. This also means that you can easily run "rsnapshot sync" on the command line without fear of forcing all the other directories to rotate up. This benefit comes at the cost of one more snapshot worth of disk space. The default is 0 (off). =back B =over 4 The amount of information to print out when the program is run. Allowed values are 1 through 5. The default is 2. 1 Quiet Show fatal errors only 2 Default Show warnings and errors 3 Verbose Show equivalent shell commands being executed 4 Extra Verbose Same as verbose, but with more detail 5 Debug All kinds of information =back B =over 4 This number means the same thing as B above, but it determines how much data is written to the logfile, if one is being written. =back B =over 4 Full filesystem path to the rsnapshot log file. If this is defined, a log file will be written, with the amount of data being controlled by B. If this is commented out, no log file will be written. =back B =over 4 This gets passed directly to rsync using the --include directive. This parameter can be specified as many times as needed, with one pattern defined per line. See the rsync(1) man page for the syntax. =back B =over 4 This gets passed directly to rsync using the --exclude directive. This parameter can be specified as many times as needed, with one pattern defined per line. See the rsync(1) man page for the syntax. =back B =over 4 This gets passed directly to rsync using the --include-from directive. See the rsync(1) man page for the syntax. =back B =over 4 This gets passed directly to rsync using the --exclude-from directive. See the rsync(1) man page for the syntax. =back B =over 4 List of short arguments to pass to rsync. If not specified, "-a" is the default. Please note that these must be all next to each other. For example, "-az" is valid, while "-a -z" is not. "-a" is rsync's "archive mode" which tells it to copy as much of the filesystem metadata as it can for each file. This specifically does *not* include information about hard links, as that would greatly increase rsync's memory usage and slow it down. If you need to preserve hard links in your backups, then add "H" to this. =back B =over 4 List of long arguments to pass to rsync. The default values are --delete --numeric-ids --relative --delete-excluded This means that the directory structure in each backup point destination will match that in the backup point source. Quotes are permitted in rsync_long_args, eg --rsync-path="sudo /usr/bin/rsync". You may use either single (') or double (") quotes, but nested quotes (including mixed nested quotes) are not permitted. Similar quoting is also allowed in per-backup-point rsync_long_args. =back B =over 4 Arguments to be passed to ssh. If not specified, the default is none. =back B =over 4 Arguments to be passed to du. If not specified, the default is -csh. GNU du supports -csh, BSD du supports -csk, Solaris du doesn't support -c at all. The GNU version is recommended, since it offers the most features. =back B B =over 4 Lockfile to use when rsnapshot is run. This prevents a second invocation from clobbering the first one. If not specified, no lock file is used. Make sure to use a directory that is not world writeable for security reasons. Use of a lock file is strongly recommended. If a lockfile exists when rsnapshot starts, it will try to read the file and stop with an error if it can't. If it *can* read the file, it sees if a process exists with the PID noted in the file. If it does, rsnapshot stops with an error message. If there is no process with that PID, then we assume that the lockfile is stale and ignore it *unless* stop_on_stale_lockfile is set to 1 in which case we stop. stop_on_stale_lockfile defaults to 0. =back B =over 4 Prevents rsync from crossing filesystem partitions. Setting this to a value of 1 enables this feature. 0 turns it off. This parameter is optional. The default is 0 (off). =back B =over 4 Changes default behavior of rsnapshot and does not initially remove the oldest snapshot. Instead it moves that directory to _delete.[processid] and continues as normal. Once the backup has been completed, the lockfile will be removed before rsnapshot starts deleting the directory. Enabling this means that snapshots get taken sooner (since the delete doesn't come first), and any other rsnapshot processes are allowed to start while the final delete is happening. This benefit comes at the cost of using more disk space. The default is 0 (off). The details of how this works have changed in rsnapshot version 1.3.1. Originally you could only ever have one .delete directory per backup level. Now you can have many, so if your next (eg) alpha backup kicks off while the previous one is still doing a lazy delete you may temporarily have extra _delete directories hanging around. =back B =over 4 LVM snapshot(s) size (lvcreate --size option). =back B =over 4 Name to be used when creating the LVM logical volume snapshot(s) (lvcreate --name option). =back B =over 4 Path to the LVM Volume Groups. =back B =over 4 Mount point to use to temporarily mount the snapshot(s). =back B /etc/ localhost/ B root@example.com:/etc/ example.com/ B rsync://example.com/path2/ example.com/ B /var/ localhost/ one_fs=1 B lvm://vg0/home/path2/ lvm-vg0/ B /usr/local/bin/backup_pgsql.sh pgsql_backup/ =over 4 Examples: B =over 4 Backs up /etc/ to /.0/localhost/etc/ using rsync on the local filesystem =back B =over 4 Backs up /usr/local/ to /.0/localhost/usr/local/ using rsync on the local filesystem =back B =over 4 Backs up root@example.com:/etc/ to /.0/example.com/etc/ using rsync over ssh =back B =over 4 Same thing but let ssh choose the remote username (as specified in ~/.ssh/config, otherwise the same as the local username) =back B =over 4 Backs up root@example.com:/usr/local/ to /.0/example.com/usr/local/ using rsync over ssh =back B =over 4 Backs up rsync://example.com/pub/ to /.0/example.com/pub/ using an anonymous rsync server. Please note that unlike backing up local paths and using rsync over ssh, rsync servers have "modules", which are top level directories that are exported. Therefore, the module should also be specified in the destination path, as shown in the example above (the pub/ directory at the end). =back B =over 4 This is the same as the other examples, but notice the fourth column. This is how you specify per-backup-point options to over-ride global settings. This extra parameter can take several options, separated by B. It is most useful when specifying per-backup rsync excludes thus: B Note the + sign. That tells rsync_long_args to I to the list of arguments to pass to rsync instead of replacing the list. The + sign is only supported for rsnapshot's rsync_long_args and rsync_short_args. =back B =over 4 Backs up the LVM logical volume called home, of volume group vg0, to /.0/lvm-vg0/. Will create, mount, backup, unmount and remove an LVM snapshot for each lvm:// entry. =back B =over 4 In this example, we specify a script or program to run. This script should simply create files and/or directories in its current working directory. rsnapshot will then take that output and move it into the directory specified in the third column. Please note that whatever is in the destination directory will be completely deleted and recreated. For this reason, rsnapshot prevents you from specifying a destination directory for a backup_script that will clobber other backups. So in this example, say the backup_database.sh script simply runs a command like: =over 4 #!/bin/sh mysqldump -uusername mydatabase > mydatabase.sql chmod u=r,go= mydatabase.sql # r-------- (0400) =back rsnapshot will take the generated "mydatabase.sql" file and move it into the /.0/db_backup/ directory. On subsequent runs, rsnapshot checks the differences between the files created against the previous files. If the backup script generates the same output on the next run, the files will be hard linked against the previous ones, and no additional disk space will be taken up. =back B B B =over 4 backup_exec simply runs the command listed. The second argument is not required and defaults to a value of 'optional'. It specifies the importance that the command return 0. Valid values are 'optional' and 'required'. If the command is specified as optional, a non-zero exit status from the command will result in a warning message being output. If the command is specified as 'required', a non-zero exit status from the command will result in an error message being output and rsnapshot itself will exit with a non-zero exit status. =back =back Remember that tabs must separate all elements, and that there must be a trailing slash on the end of every directory. A hash mark (#) on the beginning of a line is treated as a comment. Putting it all together (an example file): =over 4 # THIS IS A COMMENT, REMEMBER TABS MUST SEPARATE ALL ELEMENTS config_version 1.2 snapshot_root /.snapshots/ cmd_rsync /usr/bin/rsync cmd_ssh /usr/bin/ssh #cmd_cp /bin/cp cmd_rm /bin/rm cmd_logger /usr/bin/logger cmd_du /usr/bin/du linux_lvm_cmd_lvcreate /sbin/lvcreate linux_lvm_cmd_lvremove /sbin/lvremove linux_lvm_cmd_mount /bin/mount linux_lvm_cmd_umount /bin/umount linux_lvm_snapshotsize 2G linux_lvm_snapshotname rsnapshot linux_lvm_vgpath /dev linux_lvm_mountpath /mnt/lvm-snapshot retain alpha 6 retain beta 7 retain gamma 7 retain delta 3 backup /etc/ localhost/ backup /home/ localhost/ backup_script /usr/local/bin/backup_mysql.sh mysql_backup/ backup root@foo.com:/etc/ foo.com/ backup root@foo.com:/home/ foo.com/ backup root@mail.foo.com:/home/ mail.foo.com/ backup rsync://example.com/pub/ example.com/pub/ backup lvm://vg0/xen-home/ lvm-vg0/xen-home/ backup_exec echo "backup finished!" =back =back =head1 USAGE B can be used by any user, but for system-wide backups you will probably want to run it as root. Since backups usually get neglected if human intervention is required, the preferred way is to run it from cron. When you are first setting up your backups, you will probably also want to run it from the command line once or twice to get a feel for what it's doing. Here is an example crontab entry, assuming that backup levels B, B, B and B have been defined in B =over 4 B<0 */4 * * * /usr/local/bin/rsnapshot alpha> B<50 23 * * * /usr/local/bin/rsnapshot beta> B<40 23 * * 6 /usr/local/bin/rsnapshot gamma> B<30 23 1 * * /usr/local/bin/rsnapshot delta> =back This example will do the following: =over 4 6 alpha backups a day (once every 4 hours, at 0,4,8,12,16,20) 1 beta backup every day, at 11:50PM 1 gamma backup every week, at 11:40PM, on Saturdays (6th day of week) 1 delta backup every month, at 11:30PM on the 1st day of the month =back It is usually a good idea to schedule the larger backup levels to run a bit before the lower ones. For example, in the crontab above, notice that "beta" runs 10 minutes before "alpha". The main reason for this is that the beta rotate will pull out the oldest alpha and make that the youngest beta (which means that the next alpha rotate will not need to delete the oldest alpha), which is more efficient. A secondary reason is that it is harder to predict how long the lowest backup level will take, since it needs to actually do an rsync of the source as well as the rotate that all backups do. If rsnapshot takes longer than 10 minutes to do the "beta" rotate (which usually includes deleting the oldest beta snapshot), then you should increase the time between the backup levels. Otherwise (assuming you have set the B parameter, as is recommended) your alpha snapshot will fail sometimes because the beta still has the lock. Remember that these are just the times that the program runs. To set the number of backups stored, set the B numbers in B To check the disk space used by rsnapshot, you can call it with the "du" argument. For example: =over 4 B =back This will show you exactly how much disk space is taken up in the snapshot root. This feature requires the UNIX B command to be installed on your system, for it to support the "-csh" command line arguments, and to be in your path. You can also override your path settings and the flags passed to du using the cmd_du and du_args parameters. It is also possible to pass a relative file path as a second argument, to get a report on a particular file or subdirectory. =over 4 B =back The GNU version of "du" is preferred. The BSD version works well also, but does not support the -h flag (use -k instead, to see the totals in kilobytes). Other versions of "du", such as Solaris, may not work at all. To check the differences between two directories, call rsnapshot with the "diff" argument, followed by two backup levels or directory paths. For example: =over 4 B B B =back This will call the rsnapshot-diff program, which will scan both directories looking for differences (based on hard links). B =over 4 When B is enabled, rsnapshot must first be called with the B argument, followed by the other usual cron entries. The sync should happen as the lowest, most frequent backup level, and right before. For example: =over 4 B<0 */4 * * * /usr/local/bin/rsnapshot sync && /usr/local/bin/rsnapshot alpha> B<50 23 * * * /usr/local/bin/rsnapshot beta> B<40 23 1,8,15,22 * * /usr/local/bin/rsnapshot gamma> B<30 23 1 * * /usr/local/bin/rsnapshot delta> =back The sync operation simply runs rsync and all backup scripts. In this scenario, all calls simply rotate directories, even the lowest backup level. =back B =over 4 When B is enabled, all sync behaviour happens during an additional sync step (see above). When using the sync argument, it is also possible to specify a backup point destination as an optional parameter. If this is done, only backup points sharing that destination path will be synced. For example, let's say that example.com is a destination path shared by one or more of your backup points. =over 4 rsnapshot sync example.com =back This command will only sync the files that normally get backed up into example.com. It will NOT get any other backup points with slightly different values (like example.com/etc/, for example). In order to sync example.com/etc, you would need to run rsnapshot again, using example.com/etc as the optional parameter. =back B =over 4 Do a quick sanity check to make sure everything is ready to go. =back =head1 EXIT VALUES =over 4 B<0> All operations completed successfully B<1> A fatal error occurred B<2> Some warnings occurred, but the backup still finished =back =head1 FILES /etc/rsnapshot.conf =head1 SEE ALSO rsync(1), ssh(1), logger(1), sshd(1), ssh-keygen(1), perl(1), cp(1), du(1), crontab(1) =head1 DIAGNOSTICS Use the B<-t> flag to see what commands would have been executed. This will show you the commands rsnapshot would try to run. There are a few minor differences (for example, not showing an attempt to remove the lockfile because it wasn't really created in the test), but should give you a very good idea what will happen. Using the B<-v>, B<-V>, and B<-D> flags will print increasingly more information to STDOUT. Make sure you don't have spaces in the config file that you think are actually tabs. Much other weird behavior can probably be attributed to plain old file system permissions and ssh authentication issues. =head1 BUGS Please report bugs (and other comments) to the rsnapshot-discuss mailing list: B =head1 NOTES Make sure your /etc/rsnapshot.conf file has all elements separated by tabs. See /etc/rsnapshot.conf.default for a working example file. Make sure you put a trailing slash on the end of all directory references. If you don't, you may have extra directories created in your snapshots. For more information on how the trailing slash is handled, see the B manpage. Make sure to make the snapshot directory chmod 700 and owned by root (assuming backups are made by the root user). If the snapshot directory is readable by other users, they will be able to modify the snapshots containing their files, thus destroying the integrity of the snapshots. If you would like regular users to be able to restore their own backups, there are a number of ways this can be accomplished. One such scenario would be: Set B to B in B Set the file permissions on these directories as follows: =over 4 drwx------ /.private drwxr-xr-x /.private/.snapshots =back Export the /.private/.snapshots directory over read-only NFS, a read-only Samba share, etc. See the rsnapshot HOWTO for more information on making backups accessible to non-privileged users. For ssh to work unattended through cron, you will probably want to use public key logins. Create an ssh key with no passphrase for root, and install the public key on each machine you want to backup. If you are backing up system files from remote machines, this probably means unattended root logins. Another possibility is to create a second user on the machine just for backups. Give the user a different name such as "rsnapshot", but keep the UID and GID set to 0, to give root privileges. However, make logins more restrictive, either through ssh configuration, or using an alternate shell. BE CAREFUL! If the private key is obtained by an attacker, they will have free run of all the systems involved. If you are unclear on how to do this, see B, B, and B. Backup scripts are run as the same user that rsnapshot is running as. Typically this is root. Make sure that all of your backup scripts are only writable by root, and that they don't call any other programs that aren't owned by root. If you fail to do this, anyone who can write to the backup script or any program it calls can fully take over the machine. Of course, this is not a situation unique to rsnapshot. By default, rsync transfers are done using the --numeric-ids option. This means that user names and group names are ignored during transfers, but the UID/GID information is kept intact. The assumption is that the backups will be restored in the same environment they came from. Without this option, restoring backups for multiple heterogeneous servers would be unmanageable. If you are archiving snapshots with GNU tar, you may want to use the --numeric-owner parameter. Also, keep a copy of the archived system's /etc/passwd and /etc/group files handy for the UID/GID to name mapping. If you remove backup points in the config file, the previously archived files under those points will permanently stay in the snapshots directory unless you remove the files yourself. If you want to conserve disk space, you will need to go into the directory and manually remove the files from the smallest backup level's ".0" directory. For example, if you were previously backing up /home/ with a destination of localhost/, and alpha is your smallest backup level, you would need to do the following to reclaim that disk space: =over 4 rm -rf /alpha.0/localhost/home/ =back Please note that the other snapshots previously made of /home/ will still be using that disk space, but since the files are flushed out of alpha.0/, they will no longer be copied to the subsequent directories, and will thus be removed in due time as the rotations happen. =head1 AUTHORS Mike Rubel - B =over 4 =item - Created the original shell scripts on which this project is based =back Nathan Rosenquist (B) =over 4 =item - Primary author and original maintainer of rsnapshot. =back David Cantrell (B) =over 4 =item - Previous maintainer of rsnapshot =item - Wrote the rsnapshot-diff utility =item - Improved how use_lazy_deletes work so slow deletes don't screw up the next backup at that backup level. =back David Keegel =over 4 =item - Previous rsnapshot maintainer =item - Fixed race condition in lock file creation, improved error reporting =item - Allowed remote ssh directory paths starting with "~/" as well as "/" =item - Fixed a number of other bugs and buglets =back Benedikt Heine =over 4 =item - Current rsnapshot maintainer =back Carl Wilhelm Soderstrom B<(chrome@real-time.com)> =over 4 =item - Created the RPM .spec file which allowed the RPM package to be built, among other things. =back Ted Zlatanov (B) =over 4 =item - Added the one_fs feature, autoconf support, good advice, and much more. =back Ralf van Dooren (B) =over 4 =item - Added and maintains the rsnapshot entry in the FreeBSD ports tree. =back SlapAyoda =over 4 =item - Provided access to his computer museum for software testing. =back Carl Boe (B) =over 4 =item - Found several subtle bugs and provided fixes for them. =back Shane Leibling (B) =over 4 =item - Fixed a compatibility bug in utils/backup_smb_share.sh =back Christoph Wegscheider (B) =over 4 =item - Added (and previously maintained) the Debian rsnapshot package. =back Bharat Mediratta (B) =over 4 =item - Improved the exclusion rules to avoid backing up the snapshot root (among other things). =back Peter Palfrader (B) =over 4 =item - Enhanced error reporting to include command line options. =back Nicolas Kaiser (B) =over 4 =item - Fixed typos in program and man page =back Chris Petersen - (B) =over 4 Added cwrsync permanent-share support =back Robert Jackson (B) =over 4 Added use_lazy_deletes feature =back Justin Grote (B) =over 4 Improved rsync error reporting code =back Anthony Ettinger (B) =over 4 Wrote the utils/mysqlbackup.pl script =back Sherman Boyd =over 4 Wrote utils/random_file_verify.sh script =back William Bear (B) =over 4 Wrote the utils/rsnapreport.pl script (pretty summary of rsync stats) =back Eric Anderson (B) =over 4 Improvements to utils/rsnapreport.pl. =back Alan Batie (B) =over 4 Bug fixes for include_conf =back Dieter Bloms (B) =over 4 Multi-line configuration options =back Henning Moll (B) =over 4 stop_on_stale_lockfile =back Ben Low (B) =over 4 Linux LVM snapshot support =back =head1 COPYRIGHT Copyright (C) 2003-2005 Nathan Rosenquist Portions Copyright (C) 2002-2007 Mike Rubel, Carl Wilhelm Soderstrom, Ted Zlatanov, Carl Boe, Shane Liebling, Bharat Mediratta, Peter Palfrader, Nicolas Kaiser, David Cantrell, Chris Petersen, Robert Jackson, Justin Grote, David Keegel, Alan Batie, Dieter Bloms, Henning Moll, Ben Low, Anthony Ettinger This man page is distributed under the same license as rsnapshot: the GPL (see below). 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 =cut # more emacs-appeasement ###################################################################### ### Local Variables: ### tab-width: 4 ### End: rsnapshot-1.4.5/rsnapshot.conf.default.in000066400000000000000000000176521435251070200204740ustar00rootroot00000000000000################################################# # rsnapshot.conf - rsnapshot configuration file # ################################################# # # # PLEASE BE AWARE OF THE FOLLOWING RULE: # # # # This file requires tabs between elements # # # ################################################# ####################### # CONFIG FILE VERSION # ####################### config_version 1.2 ########################### # SNAPSHOT ROOT DIRECTORY # ########################### # All snapshots will be stored under this root directory. # snapshot_root /.snapshots/ # If no_create_root is enabled, rsnapshot will not automatically create the # snapshot_root directory. This is particularly useful if you are backing # up to removable media, such as a FireWire or USB drive. # #no_create_root 1 ################################# # EXTERNAL PROGRAM DEPENDENCIES # ################################# # LINUX USERS: Be sure to uncomment "cmd_cp". This gives you extra features. # EVERYONE ELSE: Leave "cmd_cp" commented out for compatibility. # # See the README file or the man page for more details. # #@CMD_CP@ # uncomment this to use the rm program instead of the built-in perl routine. # @CMD_RM@ # rsync must be enabled for anything to work. This is the only command that # must be enabled. # @CMD_RSYNC@ # Uncomment this to enable remote ssh backups over rsync. # #@CMD_SSH@ # Comment this out to disable syslog support. # @CMD_LOGGER@ # Uncomment this to specify the path to "du" for disk usage checks. # If you have an older version of "du", you may also want to check the # "du_args" parameter below. # #@CMD_DU@ # Uncomment this to specify the path to rsnapshot-diff. # #cmd_rsnapshot_diff @prefix@/bin/rsnapshot-diff # Specify the path to a script (and any optional arguments) to run right # before rsnapshot syncs files # #cmd_preexec /path/to/preexec/script # Specify the path to a script (and any optional arguments) to run right # after rsnapshot syncs files # #cmd_postexec /path/to/postexec/script # Paths to lvcreate, lvremove, mount and umount commands, for use with # Linux LVMs. # #linux_lvm_cmd_lvcreate @CMD_LVCREATE@ #linux_lvm_cmd_lvremove @CMD_LVREMOVE@ #linux_lvm_cmd_mount @CMD_MOUNT@ #linux_lvm_cmd_umount @CMD_UMOUNT@ ######################################### # BACKUP LEVELS / INTERVALS # # Must be unique and in ascending order # # e.g. alpha, beta, gamma, etc. # ######################################### retain alpha 6 retain beta 7 retain gamma 4 #retain delta 3 ############################################ # GLOBAL OPTIONS # # All are optional, with sensible defaults # ############################################ # Verbose level, 1 through 5. # 1 Quiet Print fatal errors only # 2 Default Print errors and warnings only # 3 Verbose Show equivalent shell commands being executed # 4 Extra Verbose Show extra verbose information # 5 Debug mode Everything # verbose 2 # Same as "verbose" above, but controls the amount of data sent to the # logfile, if one is being used. The default is 3. # loglevel 3 # If you enable this, data will be written to the file you specify. The # amount of data written is controlled by the "loglevel" parameter. # #logfile /var/log/rsnapshot # If enabled, rsnapshot will write a lockfile to prevent two instances # from running simultaneously (and messing up the snapshot_root). # If you enable this, make sure the lockfile directory is not world # writable. Otherwise anyone can prevent the program from running. # lockfile /var/run/rsnapshot.pid # By default, rsnapshot check lockfile, check if PID is running # and if not, consider lockfile as stale, then start # Enabling this stop rsnapshot if PID in lockfile is not running # #stop_on_stale_lockfile 0 # Default rsync args. All rsync commands have at least these options set. # #rsync_short_args -a #rsync_long_args --delete --numeric-ids --relative --delete-excluded # ssh has no args passed by default, but you can specify some here. # #ssh_args -p 22 # Default arguments for the "du" program (for disk space reporting). # The GNU version of "du" is preferred. See the man page for more details. # If your version of "du" doesn't support the -h flag, try -k flag instead. # #du_args -csh # If this is enabled, rsync won't span filesystem partitions within a # backup point. This essentially passes the -x option to rsync. # The default is 0 (off). # #one_fs 0 # The include and exclude parameters, if enabled, simply get passed directly # to rsync. If you have multiple include/exclude patterns, put each one on a # separate line. Please look up the --include and --exclude options in the # rsync man page for more details on how to specify file name patterns. # #include ??? #include ??? #exclude ??? #exclude ??? # The include_file and exclude_file parameters, if enabled, simply get # passed directly to rsync. Please look up the --include-from and # --exclude-from options in the rsync man page for more details. # #include_file /path/to/include/file #exclude_file /path/to/exclude/file # If your version of rsync supports --link-dest, consider enabling this. # This is the best way to support special files (FIFOs, etc) cross-platform. # The default is 0 (off). # #link_dest 0 # When sync_first is enabled, it changes the default behaviour of rsnapshot. # Normally, when rsnapshot is called with its lowest interval # (i.e.: "rsnapshot alpha"), it will sync files AND rotate the lowest # intervals. With sync_first enabled, "rsnapshot sync" handles the file sync, # and all interval calls simply rotate files. See the man page for more # details. The default is 0 (off). # #sync_first 0 # If enabled, rsnapshot will move the oldest directory for each interval # to [interval_name].delete, then it will remove the lockfile and delete # that directory just before it exits. The default is 0 (off). # #use_lazy_deletes 0 # Number of rsync re-tries. If you experience any network problems or # network card issues that tend to cause ssh to fail with errors like # "Corrupted MAC on input", for example, set this to a non-zero value # to have the rsync operation re-tried. # #rsync_numtries 0 # LVM parameters. Used to backup with creating lvm snapshot before backup # and removing it after. This should ensure consistency of data in some special # cases # # LVM snapshot(s) size (lvcreate --size option). # #linux_lvm_snapshotsize 100M # Name to be used when creating the LVM logical volume snapshot(s). # #linux_lvm_snapshotname rsnapshot # Path to the LVM Volume Groups. # #linux_lvm_vgpath /dev # Mount point to use to temporarily mount the snapshot(s). # #linux_lvm_mountpath /path/to/mount/lvm/snapshot/during/backup ############################### ### BACKUP POINTS / SCRIPTS ### ############################### # LOCALHOST backup /home/ localhost/ backup /etc/ localhost/ backup /usr/local/ localhost/ #backup /var/log/rsnapshot localhost/ #backup /etc/passwd localhost/ #backup /home/foo/My Documents/ localhost/ #backup /foo/bar/ localhost/ one_fs=1,rsync_short_args=-urltvpog #backup_script /usr/local/bin/backup_pgsql.sh localhost/postgres/ # You must set linux_lvm_* parameters below before using lvm snapshots #backup lvm://vg0/xen-home/ lvm-vg0/xen-home/ # EXAMPLE.COM #backup_exec /bin/date "+ backup of example.com started at %c" #backup root@example.com:/home/ example.com/ +rsync_long_args=--bwlimit=16,exclude=core #backup root@example.com:/etc/ example.com/ exclude=mtab,exclude=core #backup_exec ssh root@example.com "mysqldump -A > /var/db/dump/mysql.sql" #backup root@example.com:/var/db/dump/ example.com/ #backup_exec /bin/date "+ backup of example.com ended at %c" # CVS.SOURCEFORGE.NET #backup_script /usr/local/bin/backup_rsnapshot_cvsroot.sh rsnapshot.cvs.sourceforge.net/ # RSYNC.SAMBA.ORG #backup rsync://rsync.samba.org/rsyncftp/ rsync.samba.org/rsyncftp/ rsnapshot-1.4.5/t/000077500000000000000000000000001435251070200140045ustar00rootroot00000000000000rsnapshot-1.4.5/t/SysWrap.pm.in000066400000000000000000000012471435251070200163630ustar00rootroot00000000000000package SysWrap; use strict; use warnings FATAL => 'all'; use Exporter 'import'; our $VERSION = '1.0'; our @EXPORT = qw(execute rsnapshot remove_snapshot_root rsnapshot_output); sub remove_snapshot_root { my $args = shift(@_); my $snapshot_root; if(defined $args && $args ne ""){ $snapshot_root = $args; } else { $snapshot_root = "@SNAP@"; } if(-d "$snapshot_root"){ return execute("rm -rf \"$snapshot_root\""); } } sub execute { return system(@_) >> 8; } sub rsnapshot { my $args = shift(@_); return execute("@PERL@ @CWD@/rsnapshot $args >/dev/null 2>&1"); } sub rsnapshot_output { my $args = shift(@_); return `@PERL@ @CWD@/rsnapshot $args`; } 1; rsnapshot-1.4.5/t/backup_exec/000077500000000000000000000000001435251070200162555ustar00rootroot00000000000000rsnapshot-1.4.5/t/backup_exec/backup_exec.t.in000066400000000000000000000004141435251070200213170ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 2; use SysWrap; # Ensure passing behavior ok(2 == rsnapshot("-c @TEST@/backup_exec/conf/backup_exec.conf hourly")); # Ensure failing behavior ok(1 == rsnapshot("-c @TEST@/backup_exec/conf/backup_exec_fail.conf hourly")); rsnapshot-1.4.5/t/backup_exec/conf/000077500000000000000000000000001435251070200172025ustar00rootroot00000000000000rsnapshot-1.4.5/t/backup_exec/conf/backup_exec.conf.in000066400000000000000000000004661435251070200227350ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 backup_exec ls -ld /* | grep "/u" backup_exec echo "hello world!" backup_exec echo 'hello world!' backup_exec @TRUE@ backup_exec @TRUE@ optional backup_exec @TRUE@ required backup_exec @FALSE@ backup_exec @FALSE@ optional rsnapshot-1.4.5/t/backup_exec/conf/backup_exec_fail.conf.in000066400000000000000000000001541435251070200237220ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 backup_exec @FALSE@ required rsnapshot-1.4.5/t/backup_script/000077500000000000000000000000001435251070200166355ustar00rootroot00000000000000rsnapshot-1.4.5/t/backup_script/backup_script.t.in000066400000000000000000000006071435251070200222630ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 3; # pack here your amount of subtests use SysWrap; # # Test touches backup_script type backup. # ok(0 == rsnapshot("-c @TEST@/backup_script/conf/backup_script.conf sync")); ok(1 == rsnapshot("-c @TEST@/backup_exec/conf/backup_duplicate_dest_1.conf sync")); ok(1 == rsnapshot("-c @TEST@/backup_exec/conf/backup_duplicate_dest_2.conf sync")); rsnapshot-1.4.5/t/backup_script/conf/000077500000000000000000000000001435251070200175625ustar00rootroot00000000000000rsnapshot-1.4.5/t/backup_script/conf/backup_script.conf.in000066400000000000000000000003501435251070200236650ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_cp @CP@ interval hourly 2 sync_first 1 backup_script @TRUE@ a backup_script @TRUE@ b/ backup_script @TRUE@ c/1 backup_script @TRUE@ c/2 backup_script @TRUE@ c/3/ rsnapshot-1.4.5/t/backup_script/conf/backup_script_duplicate_dest_1.conf.in000066400000000000000000000002461435251070200271620ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_cp @CP@ interval hourly 2 sync_first 1 backup_script /usr/bin/true a backup_script /usr/bin/true a/ rsnapshot-1.4.5/t/backup_script/conf/backup_script_duplicate_dest_2.conf.in000066400000000000000000000002471435251070200271640ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_cp @CP@ interval hourly 2 sync_first 1 backup_script /usr/bin/true a backup_script /usr/bin/true a/b rsnapshot-1.4.5/t/cmd-post_pre-exec/000077500000000000000000000000001435251070200173225ustar00rootroot00000000000000rsnapshot-1.4.5/t/cmd-post_pre-exec/cmd-post_pre-exec.t.in000066400000000000000000000010031435251070200234240ustar00rootroot00000000000000#!@PERL@ # Testing: Only pass test, if both cmd_preexec and cmd_postexec succeeds and the exit-code matches use strict; use Test::More tests => 4; use SysWrap; ok(0 != rsnapshot("-c @TEST@/cmd-post_pre-exec/conf/pre-false-post-false.conf hourly")); ok(0 != rsnapshot("-c @TEST@/cmd-post_pre-exec/conf/pre-false-post-true.conf hourly")); ok(0 != rsnapshot("-c @TEST@/cmd-post_pre-exec/conf/pre-true-post-false.conf hourly")); ok(0 == rsnapshot("-c @TEST@/cmd-post_pre-exec/conf/pre-true-post-true.conf hourly")); rsnapshot-1.4.5/t/cmd-post_pre-exec/conf/000077500000000000000000000000001435251070200202475ustar00rootroot00000000000000rsnapshot-1.4.5/t/cmd-post_pre-exec/conf/pre-false-post-false.conf.in000066400000000000000000000002371435251070200254560ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 cmd_preexec @FALSE@ cmd_postexec @FALSE@ backup @TEMP@ cmd_pre_postexectest/ rsnapshot-1.4.5/t/cmd-post_pre-exec/conf/pre-false-post-true.conf.in000066400000000000000000000002371435251070200253430ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 cmd_preexec @FALSE@ cmd_postexec @TRUE@ backup @TEMP@ cmd_pre_postexectest/ rsnapshot-1.4.5/t/cmd-post_pre-exec/conf/pre-true-post-false.conf.in000066400000000000000000000002371435251070200253430ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 cmd_preexec @TRUE@ cmd_postexec @FALSE@ backup @TEMP@ cmd_pre_postexectest/ rsnapshot-1.4.5/t/cmd-post_pre-exec/conf/pre-true-post-true.conf.in000066400000000000000000000002361435251070200252270ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 cmd_preexec @TRUE@ cmd_postexec @TRUE@ backup @TEMP@ cmd_pre_postexectest/ rsnapshot-1.4.5/t/configtest/000077500000000000000000000000001435251070200161515ustar00rootroot00000000000000rsnapshot-1.4.5/t/configtest/conf/000077500000000000000000000000001435251070200170765ustar00rootroot00000000000000rsnapshot-1.4.5/t/configtest/conf/configtest.conf.in000066400000000000000000000002741435251070200225220ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 6 backup @TEMP@/a/ localhost/ backup_exec @TRUE@ backup_exec @TRUE@ optional backup_exec @TRUE@ required rsnapshot-1.4.5/t/configtest/configtest.t.in000066400000000000000000000002501435251070200211050ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 1; use SysWrap; # check for basic configtest ok(0 == rsnapshot("-c @TEST@/configtest/conf/configtest.conf configtest")); rsnapshot-1.4.5/t/gnu_cp/000077500000000000000000000000001435251070200152575ustar00rootroot00000000000000rsnapshot-1.4.5/t/gnu_cp/conf/000077500000000000000000000000001435251070200162045ustar00rootroot00000000000000rsnapshot-1.4.5/t/gnu_cp/conf/gnu_cp.conf.in000066400000000000000000000001671435251070200207370ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_cp @CP@ interval hourly 6 backup @TEMP@ localhost/ rsnapshot-1.4.5/t/gnu_cp/gnu_cp.t.in000066400000000000000000000006441435251070200173300ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 2; use SysWrap; # make sure snapshot_root directory does not exist before we start ok(! remove_snapshot_root(), " snapshot root does not exist before testing starts"); # check for GNU cp my @lines = `@CP@ --help 2>/dev/null | grep -i 'gnu'`; SKIP: { skip('GNU cp not found', 1) if (@lines == 0); ok(0 == rsnapshot("-c @TEST@/gnu_cp/conf/gnu_cp.conf hourly 2>&1")); } rsnapshot-1.4.5/t/link-dest_-t_when_only_one_snapshot/000077500000000000000000000000001435251070200231405ustar00rootroot00000000000000rsnapshot-1.4.5/t/link-dest_-t_when_only_one_snapshot/conf/000077500000000000000000000000001435251070200240655ustar00rootroot00000000000000link-dest_-t_when_only_one_snapshot.conf.in000066400000000000000000000003011435251070200344100ustar00rootroot00000000000000rsnapshot-1.4.5/t/link-dest_-t_when_only_one_snapshot/confconfig_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ link_dest 1 interval daily 7 backup @TEST@/link-dest_-t_when_only_one_snapshot/conf/link-dest_-t_when_only_one_snapshot.conf link_dest rsnapshot-1.4.5/t/link-dest_-t_when_only_one_snapshot/link-dest_-t_when_only_one_snapshot.t.in000066400000000000000000000012651435251070200330720ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 5; use SysWrap; # Make sure snap_root directory does not exist before we start ok(! remove_snapshot_root(), " snapshot root does not exist before testing starts"); ok(0 == rsnapshot("-c @TEST@/link-dest_-t_when_only_one_snapshot/conf/link-dest_-t_when_only_one_snapshot.conf daily"), " first rsnapshot ran"); ok(-d "@SNAP@/daily.0" && ! -d "@SNAP@/daily.1", " daily.0 exists but daily.1 does not"); ok(rsnapshot_output("-c @TEST@/link-dest_-t_when_only_one_snapshot/conf/link-dest_-t_when_only_one_snapshot.conf -t daily") =~ / --link-dest=/, " link-dest is used"); ok(0 == remove_snapshot_root(), " Removed snapshot root to clean up"); rsnapshot-1.4.5/t/long_and_short_args/000077500000000000000000000000001435251070200200205ustar00rootroot00000000000000rsnapshot-1.4.5/t/long_and_short_args/conf/000077500000000000000000000000001435251070200207455ustar00rootroot00000000000000rsnapshot-1.4.5/t/long_and_short_args/conf/long_and_short_args.conf.in000066400000000000000000000003351435251070200262360ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_cp @CP@ interval hourly 2 sync_first 1 rsync_short_args -z rsync_long_args --archive backup @TEMP@ long_and_short_args/ backup @TEMP@ long_and_short_args/ rsnapshot-1.4.5/t/long_and_short_args/conf/long_and_short_args_inline.conf.in000066400000000000000000000003551435251070200275760ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_cp @CP@ interval hourly 2 sync_first 1 backup @TEMP@ long_and_short_args_inline/ +rsync_short_args=-z backup @TEMP@ long_and_short_args_inline/ +rsync_long_args=--archive rsnapshot-1.4.5/t/long_and_short_args/long_and_short_args.t.in000066400000000000000000000005731435251070200246330ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 2; # pack here your amount of subtests use SysWrap; # # This test merely confirms rsync doesn't blow up with long & short args. Previously it did. # ok(0 == rsnapshot("-c @TEST@/long_and_short_args/conf/long_and_short_args.conf sync")); ok(0 == rsnapshot("-c @TEST@/long_and_short_args/conf/long_and_short_args_inline.conf sync")); rsnapshot-1.4.5/t/native_cp_al/000077500000000000000000000000001435251070200164305ustar00rootroot00000000000000rsnapshot-1.4.5/t/native_cp_al/conf/000077500000000000000000000000001435251070200173555ustar00rootroot00000000000000rsnapshot-1.4.5/t/native_cp_al/conf/native_cp_al.conf.in000066400000000000000000000002041435251070200232510ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ sync_first 1 link_dest 0 interval hourly 6 backup @TEMP@ native_cp_al rsnapshot-1.4.5/t/native_cp_al/native_cp_al.t.in000066400000000000000000000012351435251070200216470ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 6; use SysWrap; # # Test native_cp_al works with cmd_cp option not set and sync_first on (see contig). # ok(! remove_snapshot_root(), " snapshot root does not exist before testing starts"); ok(0 == rsnapshot("-c @TEST@/native_cp_al/conf/native_cp_al.conf sync 2>&1"), " sync success"); ok(-d "@SNAP@/.sync/native_cp_al", " sync directory exists"); ok(0 == rsnapshot("-c @TEST@/native_cp_al/conf/native_cp_al.conf hourly 2>&1"), " hourly backup after sync success"); ok(-d "@SNAP@/hourly.0/native_cp_al", " hourly backup directory exists"); ok(0 == remove_snapshot_root(), " Removed snapshot root to clean up"); rsnapshot-1.4.5/t/relative_delete_bugfix/000077500000000000000000000000001435251070200205055ustar00rootroot00000000000000rsnapshot-1.4.5/t/relative_delete_bugfix/conf/000077500000000000000000000000001435251070200214325ustar00rootroot00000000000000rsnapshot-1.4.5/t/relative_delete_bugfix/conf/relative_delete_bugfix.conf.in000066400000000000000000000001721435251070200274070ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 6 backup @TEST@/support/files/a/ localhost/ rsnapshot-1.4.5/t/relative_delete_bugfix/relative_delete_bugfix.t.in000066400000000000000000000016201435251070200257770ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 7; use SysWrap; # copy files into place mkdir("@TEST@/support/files/a") unless -d "@TEST@/support/files/a"; ok(0 == execute("cp @TEMP@/a/1 @TEMP@/a/2 @TEST@/support/files/a/")); # run rsnapshot once to copy the files into the snapshot root ok(0 == rsnapshot("-c @TEST@/rsync/conf/rsync.conf hourly")); # now remove a file from the source ok(0 == execute("rm -f @TEST@/support/files/a/1")); # run rsnapshot again, it _should_ delete the "1" file ok(0 == rsnapshot("-c @TEST@/relative_delete_bugfix/conf/relative_delete_bugfix.conf hourly")); my $path_to_check = '@TEST@/support/snapshots/hourly.0/localhost/@TEST@/support/files/a/'; # make sure the file was --deleted ok( ! -e "$path_to_check/1" ); # make sure the other one is still there ok( -e "$path_to_check/2" ); # clean up ok(0 == execute("rm -f @TEST@/support/files/a/1 @TEST@/support/files/a/2")); rsnapshot-1.4.5/t/rsnapshot_meta/000077500000000000000000000000001435251070200170335ustar00rootroot00000000000000rsnapshot-1.4.5/t/rsnapshot_meta/rsnapshot_meta.t.in000066400000000000000000000004061435251070200226540ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 1; # pack here your amount of subtests use SysWrap; # # Misc simple tests of thing that don't actually do a backup. TODO add more such tests here. # ok(`@PERL@ @CWD@/rsnapshot --version` eq "rsnapshot @VERSION@\n"); rsnapshot-1.4.5/t/rsync-exitcode/000077500000000000000000000000001435251070200167445ustar00rootroot00000000000000rsnapshot-1.4.5/t/rsync-exitcode/conf/000077500000000000000000000000001435251070200176715ustar00rootroot00000000000000rsnapshot-1.4.5/t/rsync-exitcode/conf/rsync-exitcode-bad.conf.in000066400000000000000000000001721435251070200246310ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @FALSE@ interval hourly 6 backup @TEST@/support/files/a/ localhost/ rsnapshot-1.4.5/t/rsync-exitcode/conf/rsync-exitcode-good.conf.in000066400000000000000000000001721435251070200250330ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 6 backup @TEST@/support/files/a/ localhost/ rsnapshot-1.4.5/t/rsync-exitcode/rsync-exitcode.t.in000066400000000000000000000006561435251070200225050ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 2; use SysWrap; mkdir("@TEST@/support/files/a") unless -d "@TEST@/support/files/a"; execute("cp @TEMP@/a/1 @TEMP@/a/2 @TEST@/support/files/a/"); ok(0 != rsnapshot("-c @TEST@/rsync-exitcode/conf/rsync-exitcode-bad.conf hourly")); ok(0 == rsnapshot("-c @TEST@/rsync-exitcode/conf/rsync-exitcode-good.conf hourly")); execute("rm -f @TEST@/support/files/a/1 @TEST@support/files/a/2"); rsnapshot-1.4.5/t/rsync/000077500000000000000000000000001435251070200151425ustar00rootroot00000000000000rsnapshot-1.4.5/t/rsync/conf/000077500000000000000000000000001435251070200160675ustar00rootroot00000000000000rsnapshot-1.4.5/t/rsync/conf/rsync.conf.in000066400000000000000000000001721435251070200205010ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 6 backup @TEST@/support/files/a/ localhost/ rsnapshot-1.4.5/t/rsync/rsync.t.in000066400000000000000000000005031435251070200170700ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 1; use SysWrap; mkdir("@TEST@/support/files/a") unless -d "@TEST@/support/files/a"; execute("cp @TEMP@/a/1 @TEMP@/a/2 @TEST@/support/files/a/"); ok(0 == rsnapshot("-c @TEST@/rsync/conf/rsync.conf hourly")); execute("rm -f @TEST@/support/files/a/1 @TEST@support/files/a/2"); rsnapshot-1.4.5/t/snapshot_root_with_space/000077500000000000000000000000001435251070200211145ustar00rootroot00000000000000rsnapshot-1.4.5/t/snapshot_root_with_space/conf/000077500000000000000000000000001435251070200220415ustar00rootroot00000000000000rsnapshot-1.4.5/t/snapshot_root_with_space/conf/snapshot_root_with_space.conf.in000066400000000000000000000003031435251070200304210ustar00rootroot00000000000000config_version 1.2 snapshot_root @TEST@/support/snapshots space root/ cmd_rsync @RSYNC@ interval hourly 6 backup @TEST@/snapshot_root_with_space/conf/snapshot_root_with_space.conf localhost/ rsnapshot-1.4.5/t/snapshot_root_with_space/snapshot_root_with_space.t.in000066400000000000000000000013241435251070200270160ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 5; use SysWrap; my $snap_root="@TEST@/support/snapshots space root"; # Make sure snap_root directory does not exist before we start ok(! remove_snapshot_root($snap_root), " snapshot root does not exist before testing starts"); ok(0 == rsnapshot("-c @TEST@/snapshot_root_with_space/conf/snapshot_root_with_space.conf hourly"), " first rsnapshot ran"); ok(0 == rsnapshot("-c @TEST@/snapshot_root_with_space/conf/snapshot_root_with_space.conf hourly"), " second rsnapshot ran"); ok(-d "$snap_root/hourly.0" && -d "$snap_root/hourly.1", " hourly.0 and hourly.1 directories exist afterward"); ok(0 == execute("rm -rf \"$snap_root\""), " Removed snapshot root to clean up"); rsnapshot-1.4.5/t/ssh_args/000077500000000000000000000000001435251070200156155ustar00rootroot00000000000000rsnapshot-1.4.5/t/ssh_args/conf/000077500000000000000000000000001435251070200165425ustar00rootroot00000000000000rsnapshot-1.4.5/t/ssh_args/conf/ssh_args.conf.in000066400000000000000000000002771435251070200216350ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_ssh @SSH@ interval hourly 6 ssh_args -p 22 backup @TEST_SSH_USER@@localhost:@TEST@/ssh_args/conf/ssh_args.conf localhost/ rsnapshot-1.4.5/t/ssh_args/conf/ssh_args_inline.conf.in000066400000000000000000000002751435251070200231710ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ cmd_ssh @SSH@ interval hourly 6 backup @TEST_SSH_USER@@localhost:@TEST@/ssh_args/conf/ssh_args.conf localhost/ ssh_args=-p22 rsnapshot-1.4.5/t/ssh_args/ssh_args.t.in000066400000000000000000000011151435251070200202160ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 3; use SysWrap; # make sure snap_root directory does not exist before we start ok(! remove_snapshot_root(), " snapshot root does not exist before testing starts"); SKIP: { my $ssh_test = "@SSH@ -p 22 -o StrictHostKeyChecking=no @TEST_SSH_USER@\@localhost true"; my $cant_ssh = system("$ssh_test"); skip("Cant SSH with \"$ssh_test\"", 2) if ($cant_ssh); ok(!rsnapshot("-c @TEST@/ssh_args/conf/ssh_args.conf hourly"), "ssh_args parsed"); ok(!rsnapshot("-c @TEST@/ssh_args/conf/ssh_args_inline.conf hourly"), "ssh_args_inline parsed"); } rsnapshot-1.4.5/t/support/000077500000000000000000000000001435251070200155205ustar00rootroot00000000000000rsnapshot-1.4.5/t/support/files/000077500000000000000000000000001435251070200166225ustar00rootroot00000000000000rsnapshot-1.4.5/t/support/files/template/000077500000000000000000000000001435251070200204355ustar00rootroot00000000000000rsnapshot-1.4.5/t/support/files/template/a/000077500000000000000000000000001435251070200206555ustar00rootroot00000000000000rsnapshot-1.4.5/t/support/files/template/a/1000066400000000000000000000000031435251070200207310ustar00rootroot00000000000000a1 rsnapshot-1.4.5/t/support/files/template/a/2000066400000000000000000000000031435251070200207320ustar00rootroot00000000000000a2 rsnapshot-1.4.5/t/support/skel/000077500000000000000000000000001435251070200164565ustar00rootroot00000000000000rsnapshot-1.4.5/t/support/skel/testcase000066400000000000000000000022751435251070200202220ustar00rootroot00000000000000#!@PERL@ use strict; use Test::More tests => 4; # pack here your amount of subtests use SysWrap; # # Write here in brief, what you want to check! # # your code follows here # there are MACROS, which should aways be used: # @TEST@ you're always referencing your testsuite-directory # in a test-file it could look like this: # ok(0 == rsnapshot("-c @TEST@/cmd-post_pre-exec/conf/pre-false-post-false.conf hourly")); # @SNAP@ always references a possible snapshot-root # in the configuration-file this could be one line: # snapshot_root @SNAP@ # @TEMP@ the template directory. Use this as your backup-source # in the configuration-file this could be one line: # backup @TEMP@ localhost # # there are of course other MACROS, too. check them out: # $> grep AC_SUBST configure.ac # The first parameter is always the macro_name which gets replaced #here is some pseudo-code to give you a hint, how your tests could look like ok(0 == rsnapshot("-c @TEST@/cmd-post_pre-exec/conf/pre-false-post-false.conf hourly")); # you can even skip tests SKIP: { if($condition){ skip("Cant SSH with \"$ssh_test\"", 1); } ok(!rsnapshot("-c @TEST@/ssh_args/conf/ssh_args.conf hourly"), "ssh_args parsed"); } rsnapshot-1.4.5/t/support/skel/testconf000066400000000000000000000002361435251070200202270ustar00rootroot00000000000000config_version 1.2 snapshot_root @SNAP@ cmd_rsync @RSYNC@ interval hourly 2 cmd_preexec @TRUE@ cmd_postexec @TRUE@ backup @TEMP@ cmd_pre_postexectest/ rsnapshot-1.4.5/utils/000077500000000000000000000000001435251070200147015ustar00rootroot00000000000000rsnapshot-1.4.5/utils/README000066400000000000000000000074431435251070200155710ustar00rootroot00000000000000The scripts in this folder are mostly quick and dirty examples of things that you may or may not find useful in conjunction with rsnapshot. rsnapshot-copy ------------------------------------------------------------------------------ rsnapshot-copy copies an rsnapshot snapshot root, preserving the intersnapshot hard links for unchanged files. You can use this if you want to copy/move your snapshot root to another place but find that rsync -aH on the lot is too slow (uses too much RAM). It copies intervals in chronological order using rsync --link-dest. rsnaptar ------------------------------------------------------------------------------ A script that can automatically tar up your latest snapshots and send you an e-mail when it's done. You may need to edit some of the variables by hand. backup_dpkg.sh ------------------------------------------------------------------------------ An example script to backup package info on a Debian system, to be invoked from rsnapshot using the "backup_script" parameter. If you have a Debian system, this will most likely work without changes. backup_mysql.sh ------------------------------------------------------------------------------ An example script to backup a MySQL database, to be invoked from rsnapshot using the "backup_script" parameter. You will probably need to modify this for your setup. backup_pgsql.sh ------------------------------------------------------------------------------ An example script to backup a PostgreSQL database, to be invoked from rsnapshot using the "backup_script" parameter. You will probably need to modify this for your setup. backup_rsnapshot_cvsroot.sh ------------------------------------------------------------------------------ An example script to backup the CVS repository for the rsnapshot project, which is hosted on SourceForge. This script is a good example of using the wget program to archive web content in your backups. This script is designed to be invoked from rsnapshot using the "backup_script" parameter. backup_smb_share.sh ------------------------------------------------------------------------------ An example script to backup a Microsoft SMB share, to be invoked from rsnapshot using the "backup_script" parameter. This requires the Samba program. You will definitely need to modify this for your system. debug_moving_files.sh ------------------------------------------------------------------------------ Debug script for rsync. Moves files around, so you can try backing them up to see what rsnapshot does. Only useful if you're having problems with vanishing files and want to have a known source of them. make_cvs_snapshot.sh ------------------------------------------------------------------------------ Quick and dirty shell script to find/replace "real" version numbers with a CVS-date version. ./rsnapshotdb ------------------------------------------------------------------------------ Directory containing rsnapshotDB.pl, a perl script with an XML configuration file for remote database backups. Please see ./rsnapshotdb/README.txt. rsnapshot_if_mounted.sh ------------------------------------------------------------------------------ Deprecated. A work-around for using removable drives as the snapshot root. Superceded by the "no_create_root" option in rsnapshot proper. rsnapreport.pl ------------------------------------------------------------------------------ This script just takes the output of rsnapshot using the rsync --stats option, and generates a pretty report, and tries to include errors at the bottom. sign_packages.sh ------------------------------------------------------------------------------ The shell script used to automate package signing for releases. rsnapshot-1.4.5/utils/backup_dpkg.sh000077500000000000000000000011571435251070200175160ustar00rootroot00000000000000#!/bin/sh ############################################################################## # backup_dpkg.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # This script simply backs up a list of which Debian packages are installed. # Naturally, this only works on a Debian system. # # This script simply needs to dump a file into the current working directory. # rsnapshot handles everything else. ############################################################################## # $Id: backup_dpkg.sh,v 1.2 2005/04/02 07:37:06 scubaninja Exp $ /usr/bin/dpkg --get-selections > dpkg_selections rsnapshot-1.4.5/utils/backup_mysql.sh000077500000000000000000000020671435251070200177370ustar00rootroot00000000000000#!/bin/sh ############################################################################## # backup_mysql.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # This is a simple shell script to backup a MySQL database with rsnapshot. # # The assumption is that this will be invoked from rsnapshot. Also, since it # will run unattended, the user that runs rsnapshot (probably root) should have # a .my.cnf file in their home directory that contains the password for the # MySQL root user. For example: # # /root/.my.cnf (chmod 0600) # [client] # user = root # password = thepassword # host = localhost # # This script simply needs to dump a file into the current working directory. # rsnapshot handles everything else. ############################################################################## # $Id: backup_mysql.sh,v 1.6 2007/03/22 02:50:21 drhyde Exp $ umask 0077 # backup the database /usr/bin/mysqldump --all-databases > mysqldump_all_databases.sql # make the backup readable only by root /bin/chmod 600 mysqldump_all_databases.sql rsnapshot-1.4.5/utils/backup_pgsql.sh000077500000000000000000000017721435251070200177220ustar00rootroot00000000000000#!/bin/sh ############################################################################## # backup_pgsql.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # This is a simple shell script to backup a PostgreSQL database with rsnapshot. # # The assumption is that this will be invoked from rsnapshot. Also, since it # will run unattended, the user that runs rsnapshot (probably root) should have # a .pgpass file in their home directory that contains the password for the # postgres user. For example: # # /root/.pgpass (chmod 0600) # *:*:*:postgres:thepassword # # This script simply needs to dump a file into the current working directory. # rsnapshot handles everything else. ############################################################################## # $Id: backup_pgsql.sh,v 1.6 2007/03/22 02:50:21 drhyde Exp $ umask 0077 # backup the database /usr/local/pgsql/bin/pg_dumpall -Upostgres > pg_dumpall.sql # make the backup readable only by root /bin/chmod 600 pg_dumpall.sql rsnapshot-1.4.5/utils/backup_rsnapshot_cvsroot.sh000077500000000000000000000016031435251070200223650ustar00rootroot00000000000000#!/bin/sh ############################################################################## # backup_rsnapshot_cvsroot.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # This is a simple shell script to backup the CVS tar/bz file from # SourceForge. # # The assumption is that this will be invoked from rsnapshot. Also, since it # will run unattended, the user that runs rsnapshot (probably root) should have # a .pgpass file in their home directory that contains the password for the # postgres user. # # This script simply needs to dump a file into the current working directory. # rsnapshot handles everything else. ############################################################################## # $Id: backup_rsnapshot_cvsroot.sh,v 1.3 2005/04/02 07:37:07 scubaninja Exp $ /usr/bin/wget http://cvs.sourceforge.net/cvstarballs/rsnapshot-cvsroot.tar.bz2 2>/dev/null rsnapshot-1.4.5/utils/backup_smb_share.sh000077500000000000000000000030601435251070200205270ustar00rootroot00000000000000#!/bin/sh ############################################################################## # backup_smb_share.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # This is a simple shell script to backup an SMB share with rsnapshot. # # The assumption is that this will be invoked from rsnapshot. Also, for # security reasons, the authfile should be stored in a place where it is not # accessible by anyone other than the user running rsnapshot (probably root). # # This script simply needs to dump the contents of the SMB share into the # current working directory. rsnapshot handles everything else. # # Please note that because of cross-platform issues, the files archived will # be owned by the user running rsnapshot to make the backup, not by the # original owner of the files. Also, any ACL permissions that may have been # on the Windows machine will be lost. However, the data in the files will # be archived safely. ############################################################################## # $Id: backup_smb_share.sh,v 1.6 2005/04/02 07:37:07 scubaninja Exp $ # IP or hostname to backup over SMB SERVER=192.168.1.10 # The name of the share SHARE=home # The authfile is a file that contains the username and password to connect # with. man smbclient(1) for details on how this works. It's much more secure # than specifying the password on the command line directly. AUTHFILE=/path/to/authfile # connect to the SMB share using the authfile /usr/local/samba/bin/smbclient //${SERVER}/${SHARE} -A ${AUTHFILE} -Tc - 2>/dev/null | tar xf - rsnapshot-1.4.5/utils/debug_moving_files.sh000077500000000000000000000023141435251070200210670ustar00rootroot00000000000000#!/bin/sh ############################################################################## # debug_moving_files.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # This is an abusive shell script designed to simulate users moving files # while a backup is occuring. Hopefully your users aren't this evil. # # The general idea is that you create an "abuse" directory, include it in your # backup points, run this script in one terminal window and rsnapshot in the # other. # # Most people will probably never need to use this unless they want to debug # "vanishing file" problems with rsync. ############################################################################## # $Id: debug_moving_files.sh,v 1.3 2005/04/02 07:37:07 scubaninja Exp $ # change this path to your liking DIRECTORY=/path/to/abuse/dir/ # be sure and create it or the script won't work cd $DIRECTORY || exit 1; # ready touch 0 1 2 3 4 5 6 7 8 9 rm *.tmp 2>/dev/null # set echo "STARTING ABUSE NOW..." # go while true; do # move them for i in `echo 0 1 2 3 4 5 6 7 8 9`; do echo mv $i $i.tmp mv $i $i.tmp done # move them back for i in `echo 0 1 2 3 4 5 6 7 8 9`; do echo mv $i.tmp $i mv $i.tmp $i done done rsnapshot-1.4.5/utils/make_cvs_snapshot.sh000077500000000000000000000014571435251070200207560ustar00rootroot00000000000000#!/bin/sh ############################################################################## # make_cvs_snapshot.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # # this script just does a find/replace in the source tree to change # the version number to CVS-$DATE # # this was done before manually, now it's automatic ############################################################################## # $Id: make_cvs_snapshot.sh,v 1.6 2005/06/18 02:30:53 scubaninja Exp $ if [ $PWD = "$HOME/projects/rsnapshot/rsnapshot" ]; then echo "This is not where you want to be. cp -r to a different directory first!" echo "Quitting now!" exit 1 fi VERSION=`./rsnapshot-program.pl version-only | sed s/\\\./\\\\\\\./g` DATE=`date +"%Y%m%d"` perl -pi -e s/$VERSION/CVS-$DATE/g `find . -type f` rsnapshot-1.4.5/utils/mkmakefile.sh000077500000000000000000000012361435251070200173470ustar00rootroot00000000000000#! /bin/sh -xe # This mini script is to generate the Makefile for building rsnapshot. # # Usage: mkmakefile.sh [options to pass to ./configure] # # Example: mkmakefile.sh # Example: mkmakefile.sh --sysconfdir=/etc # # You can just re-run ./configure after running mkmakefile.sh to change the # options to ./configure. # # Inputs: Makefile.am configure.ac # Outputs: Makefile Makefile.in configure aclocal.m4 # # This script is executed with the sh -e flag, so that an error from # executing any command will cause the shell script to abort immediately. # trap "echo Previous command had error, mkmakefile.sh aborting." ERR # aclocal autoconf automake ./configure "$@" rsnapshot-1.4.5/utils/mysqlbackup.pl000066400000000000000000000054611435251070200175770ustar00rootroot00000000000000#!/usr/bin/perl =head1 Author: Anthony Ettinger License: GPL 2.0 URL: http://www.gnu.org/licenses/gpl.txt Notes: This script was originally written to function as a MySQL database backup script in conjunction with the open source Perl/rsync backup program "rsnapshot". rsnapshot can be found here: http://www.rsnapshot.org/ In order to backup a MySQL database remotely, the necessary database user must be able to connect remotely to the database server from your IP number (some ISPs only allow access from localhost - you may need to email your admin and ask for your ip to be given access) It is extremely important that you secure the /etc/mysqlbackup file so only YOU can read the file, 'chmod 0600 /etc/mysqlbackup', as it will store the database passwords in plain text format. =cut use warnings; use strict; use Data::Dumper; use DBI; use POSIX qw(strftime); ## WARNING: type 'chmod 0600 /etc/mysqlbackup' ## #file must contain 'username:password:host' #one entry per line. Functionality is similar to /etc/passwd, #however passwords are stored in plain text and NOT encrypted my $mysqlbackup_passwd = '/etc/mysqlbackup'; #location of 'mysqldump' program (required) my $mysqldump = '/usr/bin/mysqldump'; main(); sub main { #check mode of $mysqlbackup_passwd file my ($mode) = (stat($mysqlbackup_passwd))[2]; $mode = sprintf "%04o", $mode & 07777; unless (-o $mysqlbackup_passwd && $mode eq '0600') { die "Please secure '$mysqlbackup_passwd' file. Type 'chmod 0600 $mysqlbackup_passwd'.\n"; } #read in passwords from file read_passwd(); } sub read_passwd { open(PASSWD, $mysqlbackup_passwd) or die "$!"; while() { chomp; my ($user, $pass, $host) = split(/:/); #retrieve databases with this user's privileges show_databases($user, $pass, $host); } close(PASSWD); } sub show_databases { my ($user, $pass, $host) = @_; my $db_list = []; #arrayref to store list of databases my $dbh = DBI->connect("dbi:mysql:host=$host", $user, $pass) or die DBI->errstr; #execute show databases query my $sth = $dbh->prepare("SHOW DATABASES") or die $dbh->errstr; $sth->execute() or die $dbh->errstr; #fetch results from query while (my $db_row = $sth->fetch) { push(@{$db_list}, $db_row->[0]); } dump_databases($db_list, $user, $pass, $host); } sub dump_databases { my ($db_list, $user, $pass, $host) = @_; my $timestamp = strftime "%F-%H.%M", localtime; foreach my $db (@{$db_list}) { my $filename = "$host-$db-$timestamp"; my $dump_cmd = "$mysqldump -u $user -p$pass -h $host --opt $db > $filename.sql"; my $tar_cmd = "tar czf $filename.tar.gz $filename.sql"; my $rm_cmd = "rm $filename.sql"; #print "Backing up $db from $host\n"; system($dump_cmd) == 0 or die "$!"; system($tar_cmd) == 0 or die "$!"; system($rm_cmd) == 0 or die "$!"; #tar czf $db.$DATE.tar.gz $FILE } } rsnapshot-1.4.5/utils/random_file_verify.sh000077500000000000000000000040761435251070200211120ustar00rootroot00000000000000#!/bin/bash ############################################################################## # random_file_verify.sh # # by Sherman Boyd # http://www.rsnapshot.org/ # # This script generates a random test file to be backed up and # then tests it after the backup. It's a paranoid sanity check, I # suppose. The script then emails the results of the backup and check # to the admin. # # As it is currently implemented, it is a wrapper around an "rsnapshot daily" # command. It could probably be broken out into seperate preexec annd # postexec commands as well. # # Feel free to use and improve ... # ############################################################################## #Initialize echo "Started backup script on `date`"> /var/log/localbackup.log msubject="Local Backup SUCCESSFUL!" #Create random 100 byte file echo "Generating random test file.">> /var/log/localbackup.log dd if=/dev/urandom of=/path/to/files/you/are/backing/up/randomtestfile bs=1 count=100 if [ $? -eq 0 ] then echo "SUCCESS: Randomly generated test file created." >>\ /var/log/localbackup.log else echo "FAILED: Randomly generated test file not created." >>\ /var/log/localbackup.log msubject="Local Backup has ERRORS!" fi #Run Backup echo "Running rsnapshot backup.">> /var/log/localbackup.log rsnapshot daily if [ $? -eq 0 ] then echo "SUCCESS: Backup completed with no errors." >> /var/log/localbackup.log else echo "FAILED: Backup completed with some errors." >> /var/log/localbackup.log msubject="Local Backup has ERRORS!" fi #Test Random File echo "Comparing random file with the backup.">> /var/log/localbackup.log diff /path/to/files/you/are/backing/up/randomtestfile /path/to/your/rsnapshots/daily.0/localhost/randomtestfile > /dev/null if [ $? -eq 0 ] then echo "PASSED: Randomly generated test file is the same." >>\ /var/log/localbackup.log else echo "FAILED: Randomly generated test file differs." >>\ /var/log/localbackup.log msubject="Local Backup has ERRORS!" fi #Mail results mail -s "$msubject" your@email.com < /var/log/localbackup.log exit 0 rsnapshot-1.4.5/utils/rsnapreport.pl000066400000000000000000000123361435251070200176220ustar00rootroot00000000000000#!/usr/bin/env perl # this script prints a pretty report from rsnapshot output # in the rsnapshot.conf you must set # verbose >= 4 # and add --stats to rsync_long_args # then setup crontab 'rsnapshot daily 2>&1 | rsnapreport.pl | mail -s"SUBJECT" backupadm@adm.com # don't forget the 2>&1 or your errors will be lost to stderr # If you would prefer to leave the rsnapshot.conf verbose value unchanged, # an alternative is to pass the -V option to rsnapshot. # For example: rsnapshot -V daily 2>&1 | rsnapreport.pl ################################ ## Copyright 2006 William Bear ## 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 St, Fifth Floor, Boston, MA 02110-1301 USA ################################ use strict; use warnings; use English '-no_match_vars'; use File::Spec; # splitdir() my $bufsz = 2; my %bkdata=(); my @errors=(); sub pretty_print(){ my $ofh = select(STDOUT); $FORMAT_NAME="BREPORTBODY"; $FORMAT_TOP_NAME="BREPORTHEAD"; select($ofh); foreach my $source (sort keys %bkdata){ if($bkdata{$source} =~ /error/i) { print "ERROR $source $bkdata{$source}"; next; } my $files = $bkdata{$source}{'files'}; my $filest = $bkdata{$source}{'files_tran'}; my $filelistgentime = $bkdata{$source}{'file_list_gen_time'}; my $filelistxfertime = $bkdata{$source}{'file_list_trans_time'}; my $bytes = $bkdata{$source}{'file_size'}/1000000; # convert to MB my $bytest = $bkdata{$source}{'file_tran_size'}/1000000; # convert to MB $source =~ s/^[^\@]+\@//; # remove username format BREPORTHEAD = SOURCE TOTAL FILES FILES TRANS TOTAL MB MB TRANS LIST GEN TIME LIST XFER TIME -------------------------------------------------------------------------------------------------------------------- . format BREPORTBODY = @<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>>> @>>>>>>>>>> @#########.## @########.## @>>>>>>>>>>>> @>>>>>>>>>>>>> $source, $files, $filest, $bytes, $bytest, $filelistgentime, $filelistxfertime . write STDOUT; } } sub nextLine($){ my($lines) = @_; my $line = <>; push(@$lines,$line); return shift @$lines; } my $linux_lvm_lv = undef; my @rsnapout = (); # load readahead buffer for(my $i=0; $i < $bufsz; $i++){ $rsnapout[$i] = <>; } while (my $line = nextLine(\@rsnapout)){ while($line =~ /\s+\\$/){ # combine wrapped lines $line =~ s/\\$//g; $line .= nextLine(\@rsnapout); } if($line =~ /^[\/\w]+\/lvcreate\h+-[-\w]/) { # Look for LVM snapshot # Extract the LVM logical volume from the lvcreate command. my $lvpath = (split /\s+/, $line)[-1]; my ($vg, $lv) = (File::Spec->splitdir($lvpath))[-2,-1]; $linux_lvm_lv = 'lvm://' . $vg . '/' . $lv . '/'; } # find start rsync command line elsif($line =~ /^[\/\w]+\/rsync\h+-[-\w]/) { my @rsynccmd=(); push(@rsynccmd,split(/\s+/,$line)); # split into command components my $source; # Use LVM logical volume name if it exists. if ($linux_lvm_lv) { $source = $linux_lvm_lv; } else { # count backwards: source always second to last $source = $rsynccmd[-2]; } #print $source; while($line = nextLine(\@rsnapout)){ # this means we are missing stats info if($line =~ /^[\/\w]+\/rsync\h+-[-\w]/){ unshift(@rsnapout,$line); push(@errors,"$source NO STATS DATA"); last; } # stat record if($line =~ /^total size is\s+\d+/){ last; } # this ends the rsync stats record # Number of files: 1,325 (reg: 387, dir: 139, link: 799) elsif($line =~ /Number of files:\s+([\d,]+)/){ $bkdata{$source}{'files'}=$1; $bkdata{$source}{'files'}=~ s/,//g; } # Number of regular files transferred: 1 elsif($line =~ /Number of (regular )?files transferred:\s+([\d,]+)/){ $bkdata{$source}{'files_tran'}=$2; } # Total file size: 1,865,857 bytes elsif($line =~ /Total file size:\s+([\d,]+)/){ $bkdata{$source}{'file_size'}=$1; $bkdata{$source}{'file_size'}=~ s/,//g; } elsif($line =~ /Total transferred file size:\s+([\d,]+)/){ $bkdata{$source}{'file_tran_size'}=$1; $bkdata{$source}{'file_tran_size'}=~ s/,//g; } elsif($line =~ /File list generation time:\s+(.+)/){ $bkdata{$source}{'file_list_gen_time'}=$1; } elsif($line =~ /File list transfer time:\s+(.+)/){ $bkdata{$source}{'file_list_trans_time'}=$1; } elsif($line =~ /^(rsync error|ERROR): /){ push(@errors,"$source $line"); } # we encountered an rsync error } # If this was a logical volume, we are done with it. $linux_lvm_lv = undef; } elsif($line =~ /^(rsync error|ERROR): /){ push(@errors,$line); } # we encountered an rsync error } pretty_print(); if(scalar @errors > 0){ print "\nERRORS\n"; print join("\n",@errors); print "\n"; } rsnapshot-1.4.5/utils/rsnapshot-copy000066400000000000000000000045421435251070200176220ustar00rootroot00000000000000#!/bin/bash function usage { cat <<'EOM' >&2 Usage: rsnapshot-copy [--test] RSYNC-OPTION... SRC-SNAPSHOT-ROOT DEST-SNAPSHOT-ROOT rsnapshot-copy copies an rsnapshot snapshot root, preserving the intersnapshot hard links for unchanged files. It does so by copying one snapshot at a time with rsync, each time giving a --link-dest option for the previous snapshot. This technique uses less memory than others (such as plain rsync -H) that hold a list of all the files in the snapshot root in memory in order to preserve arbitrary hard links. As with an ordinary rsync command, either the source or the destination can be remote, but not both. The destination should be empty or nonexistent; rsnapshot-copy currently is not designed for incremental mirroring of a snapshot root (though an incremental mode may be added in the future). Each snapshot is copied using `rsync RSYNC-OPTION...', so you should probably pass -a and any other relevant rsync options used by your ordinary rsnapshot runs, such as --numeric-ids. --test: show the shell commands to be executed instead of executing them Written and maintained by Matt McCutchen . EOM } # To support remote src or dest, we must perform all filesystem access via rsync. set -e trap 'echo "Command failed!: $BASH_COMMAND" >&2' ERR set -o errtrace set -o pipefail testmode= if [ "$1" = "--test" ]; then testmode=1 shift fi # Test or execute a command as appropriate. function do_cmd { if [ $testmode ]; then (set -x; : "$@") else "$@" fi } if [ $# -lt 3 ]; then usage exit 1 fi rsync_opts=("${@:1:$#-2}") src="${@: -2:1}" dest="${@: -1:1}" # Ensure that the destination dir exists. (--exclude=* excludes everything else.) do_cmd rsync "${rsync_opts[@]}" --exclude=* "$src/" "$dest/" # Obtain a list of snapshot names in newest to oldest order. # List the src -> filter to `2008/04/09T17:59:43 alpha.0' format # -> sort newest to oldest -> read each line, ignoring the time. rsync "${rsync_opts[@]}" --list-only --no-r -d --no-l "$src/" \ | sed -nre 's,^d[^ ]+ +[^ ]+ (..../../..) (..:..:..) (.*\.[0-9]+)$,\1T\2 \3,p' \ | LC_ALL=C sort -r --key=1,1 | { # --link-dest option to use, if any ldo=() # Copy each snapshot while read st sn; do do_cmd rsync "${rsync_opts[@]}" "${ldo[@]}" "$src/$sn/" "$dest/$sn/" # The next snapshot should link from this one. ldo=(--link-dest="../$sn/") done } rsnapshot-1.4.5/utils/rsnapshot_if_mounted.sh000077500000000000000000000034651435251070200215020ustar00rootroot00000000000000#!/bin/sh ############################################################################## # rsnapshot_if_mounted.sh # # by Nathan Rosenquist # http://www.rsnapshot.org/ # ############################################################################## ############################################################################## ############################################################################## # # NOTE: THIS SCRIPT HAS BEEN SUPERCEDED BY THE "no_create_root" OPTION IN # rsnapshot. IT IS LEFT HERE JUST IN CASE ANYONE WANTS TO USE IT. # ############################################################################## ############################################################################## ############################################################################## # # This is a simple shell script to run rsnapshot only if the backup drive # is mounted. It is intended to be used when backups are made to removable # devices (such as FireWire drives). # # Usage: /path/to/rsnapshot_if_mounted.sh [options] interval # # Edit this script so it points to your rsnapshot program and snapshot root. # Then simply call this script instead of rsnapshot. # # Example: /usr/local/bin/rsnapshot_if_mounted.sh -v daily ############################################################################## # $Id: rsnapshot_if_mounted.sh,v 1.4 2005/04/02 07:37:07 scubaninja Exp $ # path to rsnapshot RSNAPSHOT=/usr/local/bin/rsnapshot # snapshot_root SNAPSHOT_ROOT=/.snapshots/; # external programs LS=/bin/ls HEAD=/usr/bin/head # check to see if the drive is mounted IS_MOUNTED=`$LS $SNAPSHOT_ROOT/ | $HEAD -1` > /dev/null 2>&1; # if the drive is mounted, run rsnapshot # otherwise refuse to run if [ $IS_MOUNTED ]; then $RSNAPSHOT $@ else echo "$SNAPSHOT_ROOT is not mounted, rsnapshot will not be run" fi rsnapshot-1.4.5/utils/rsnapshot_invert.sh000077500000000000000000000017401435251070200206520ustar00rootroot00000000000000#!/bin/bash # $Id: rsnapshot_invert.sh,v 1.1 2007/04/12 16:51:58 drhyde Exp $ # This script takes one parameter, which should be your rsnapshot config # file. It will parse that file to find your snapshot_root, backup points, # and interval/retain values, and will create from those an inverted # directory structure of backup points containing daily.{0,1,2,3} etc # symlinks. Run it from a cron job to keep that structure up to date. # # There is minimal^Wno error checking, and the parsing is totally brain- # dead. SNAPSHOT_ROOT=`grep ^snapshot_root $1|awk '{print \$2}'` BACKUPS=`grep ^backup $1|awk '{print \$3}'` INTERVALS=`grep -E '^(interval|retain)' $1|awk '{print \$2}'` cd $SNAPSHOT_ROOT for i in $BACKUPS; do mkdir $i for j in $INTERVALS; do HOWMANY=`grep -E ^\(interval\|retain\).$j $1|awk '{print \$3}'` COUNT=0 while [[ $COUNT != $HOWMANY ]]; do ln -s $SNAPSHOT_ROOT/$j.$COUNT/$i $SNAPSHOT_ROOT/$i/$j.$COUNT COUNT=$(($COUNT + 1)) done done done rsnapshot-1.4.5/utils/rsnapshotdb/000077500000000000000000000000001435251070200172305ustar00rootroot00000000000000rsnapshot-1.4.5/utils/rsnapshotdb/CHANGES.txt000066400000000000000000000015541435251070200210460ustar00rootroot0000000000000010/22/06 Removed requirement that a database password be present in rsnapshotDB.conf xml file. 8/16/06 Changed permission of log file /var/log/rsnapshotDB to 0600. When passwords are used to access the database, they are shown in the log file when verbosity is enabled. 1/22/06 Cleaned up code and removed password requirement for ssh server connection. Removed requirement of Net::SSH::Perl Speed optimization done with remote database dump and gzip compression before transfering file locally. 1/4/05 Switched to XML-based configuration file, and requirement of Net::SSH::Perl for more sophisticated ssh handling of database backups. Speed issue of remote database dumps should be slightly if not vastly more optimized in this release. Database tables were locking during a remote backup, which took down the web site temporarily ( ~ 30 mins on one forum I own). rsnapshot-1.4.5/utils/rsnapshotdb/INSTALL.txt000066400000000000000000000034201435251070200210760ustar00rootroot00000000000000rsnapshotDB has a few necessary requirements pre-install, as follows Pre-install Requirements: 1) Perl v. 5.8.7+ 2) SSH with ssh-keygen 3) Unix commands: scp, rm, gzip, mysqldump/pg_dump, nice These need to be inplace on both the remote and/or local server. If using XML conf, then definitely on both servers for scp, rm, gzip, and 'nice'. The "dumper" programs can be only on the remote server since rsnapshotDB version: 1.2. 5) ssh-keygen public key on remote server Ideally, you would automate easily by having public/private ssh keys on your servers. See ~/.ssh and 'man ssh-keygen'. 6) local access to database server This would also negate the need of storing vital (ie - "password") info in the XML config file. Note: This hasn't been thoroughly tested yet! Please report any bugs to mailto:rsnapshot-discuss-request@lists.sf.net?subject=subscribe http://www.rsnapshot.org 7) Copy rsnapshotDB.conf.sample to /etc/rsnapshotDB.conf BASIC INSTALL: Place the rsnapshotDB.conf and rsnapshotDB.xsd in a safe location (typicall /etc/) then type $ chmod 600 rsnapshotDB.* Copy rsnapshotDB.pl into a good location, something like: /usr/local/bin/rsnapshotDB.pl Then call it from the rsnapshot.conf file as: backup_script /usr/local/bin/rsnapshotDB.pl database/ SECURITY: The best option is to use ssh keys and network-authenticated database users. ie (only connect from localhost or 192.168.x.x -> db server without password). This avoids having to store your db passwords in /etc/rsnapshotDB.conf NOTE: If you have not logged in via ssh and keys before, you will have to do so manually as the rsnapshot user (typically root). This is necessary in order to accept the host as an authorized host. Once you say "yes" to accepting the key/host, you should be able to login automatically. rsnapshot-1.4.5/utils/rsnapshotdb/LICENSE.txt000066400000000000000000000431331435251070200210570ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, 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 Library 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 St, 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 Library General Public License instead of this License. rsnapshot-1.4.5/utils/rsnapshotdb/README.txt000066400000000000000000000006621435251070200207320ustar00rootroot00000000000000See LICENSE.txt for licensing information See INSTALL.txt for installation instructions See TODO.txt for more to come For latest documentation type: $ perldoc rsnapshotDB.pl or $ perldoc rsnapshotDB or $ perldoc /path/to/rsnapshotDB.pl Trouble Shooting: mailing list: rsnapshot-request@lists.sf.net subject: "subscribe" Other Info: Author: Anthony Ettinger Email: aettinger<-rm-@-spam->sdsualumni.org Blog: http://www.chovy.com rsnapshot-1.4.5/utils/rsnapshotdb/TODO.txt000066400000000000000000000016121435251070200205360ustar00rootroot00000000000000TODO LIST: - as suggested by r00t in #bash, add option to split on tables instead of db. ie: ./hostname/database/table.tar.gz - enable split | gzip to get in under isp radar (cpu resource limits with I/O on large db's). - thoroughly test with postgresql, mysql on Linux only testing so far. 10/22/06 DONE - remove requirement for database password to be present in xml config file. 8/16/06 DONE - changed log file to 0600 permissions 1/22/06 DONE - Thoroughly test rewrite using ssh keys and tunneling. DONE - Optimize code efficiency, main reason I'm throwing this out on the web as GPL :-) - Decide whether to stop, or continue on errors/connectivity issues. Possibly throw an email/SMS at someone. Problem because assumption is that you're db's are being archived without error. DONE - remove support for unencrypted SSH passwords in rsnapshotDB.conf, should be supported with ssh keys instead. rsnapshot-1.4.5/utils/rsnapshotdb/rsnapshotDB.conf.sample000066400000000000000000000033601435251070200236100ustar00rootroot00000000000000 mysql db.example.com user pass ssh2 foo.example.net admin StronglyEncryptedHash mysql db.example.org user2 #F0oB4r! mysql db.example.com me plain-text-password ssh2 www.example.com user optional rsnapshot-1.4.5/utils/rsnapshotdb/rsnapshotDB.pl000077500000000000000000000342251435251070200220250ustar00rootroot00000000000000#!/usr/bin/perl =pod =head1 NAME: rsnapshotDB Web: http://www.rsnapshot.org Bugs: rsnapshot-discuss@lists.sf.net License: GPL http://www.gnu.org/licenses/gpl.txt Version: 1.2.1 =head1 AUTHOR: Anthony Ettinger Email: aettinger<--at-->sdsualumni<--dot-->org Blog: http://anthony.ettinger.name =head1 DESCRIPTION: This script was originally written to function as a MySQL database backup script in conjunction with the open source Perl/rsync backup program "rsnapshot". rsnapshot can be found at: http://www.rsnapshot.org/ In order to backup a database remotely, the necessary database user must be able to connect remotely to the database server from a trusted secure shell server. (some ISPs only allow access from an internal network - you may need to make sure you do have internal access from an internal ssh server to the database server). IF YOU DON'T HAVE SSH KEYS, this program isn't for you. (see:man ssh-keygen). It is extremely important that you secure the /etc/rsnapshotDB.conf file so only YOU (the user who's cronjob this is running from) can read the file, 'chmod 0600 /etc/rsnapshotDB.conf', as it will store the database passwords in plain text format. If you don't know who YOU are - type 'whoami' or ask a friend. For best results, configure and run this script from /etc/rsnapshot.conf. (see:'man rsnapshot', backup_script). =head2 SEE ALSO: INSTALL.txt, TODO.txt, CHANGES.txt =cut use warnings; use strict; use Cwd 'cwd'; use Data::Dumper; use DBI; use POSIX qw(strftime); =head3 WARNING: type 'chmod 0600 /etc/rsnapshotDB.conf' Currently 'dbtype' supported can be either 'mysql' or 'pgsql' Functionality is similar to /etc/DBPASSWD, however passwords are stored in plain text and NOT encrypted are allowed in the following file: Note: rsnapshotdb.list is deprecated in favor of XML config rsnapshotDB.conf and rsnapshotDB.xsd =cut my $dbpasswd = '/etc/rsnapshotDB.conf'; my $xsd = '/etc/rsnapshotDB.xsd'; #used to validate config file my $xmlUsage = 1; #0 if using flat-list configuation file (deprecated). my $verbose = 2; #0 for no warning/status messages, increase for more. =head2 WARNING: Setting the "temporary" directory: 1) the db dump might get left behind on error 2) the temp directory could fill up, depending on size of db and quota of user or directory =cut my $tmpDir = '$HOME/tmp'; #may want to change this^ my $niceness = '19'; #amount of CPU/Mem -20 high, 19 low priority. my $sshOption = '-o TCPKeepAlive=yes'; #keep ssh alive (avoid timeouts) =head2 DUMPERS: Location of "dumper" program(s) type 'which ' to find the path (ie - 'which mysqldump') Note: the hash key here must match 'dbtype' field in $dbpasswd file. =cut my $dbApp = { 'mysql' => { 'dumper' => { bin => 'mysqldump', opts => '--opt -C', user => '-u', pass => '-p', host => '-h', }, 'prompt' => { bin => 'mysql', opts => '-s', user => '-u', pass => '-p', host => '-h', }, }, 'pgsql' => { 'dumper' => { bin => 'pg_dump', opts => '', user => '-U', pass => '-p', host => '-h', }, 'prompt' => { bin => 'pgsql', opts => '', user => '-U', pass => '-p', host => '-h', }, }, }; init(); sub init { #check mode of $dbpasswd file my ($mode_dbpasswd) = (stat($dbpasswd))[2]; $mode_dbpasswd = sprintf "%04o", $mode_dbpasswd & 07777; my $localTmpDir = cwd(); unless (-o $dbpasswd && $mode_dbpasswd eq '0600') { die "Please secure '$dbpasswd' file. Type 'chmod 0600 $dbpasswd'.\n"; } unless ($xmlUsage && -f $xsd) { warn "You are not validating '$dbpasswd' against an XMLSchema file: '$xsd'. Defaulting to flat file format for '$dbpasswd'.\n"; } #read in passwords from file read_dbpasswd(); } sub read_dbpasswd { if ($xmlUsage) { my $xobj = rsnapshotDB->new( { 'dbpasswd' => $dbpasswd, 'xsd' => $xsd, 'dbApp' => $dbApp, 'tmpDir' => $tmpDir, 'verbose' => $verbose, } ); my $validity = $xobj->validateXML; #boolean test if ($validity) { #main module dump routine called within my $status = $xobj->parseXML(); } } else { die "flat list is deprecated, please see INSTALL.txt"; } } =pod =head1 END OF THE LINE: If you've gotten this far with no "die" errors, you should be good to go with XML config rsnapshotDB.conf vs. flat list rsnapshotdb.list Check the $localTmpDir or your /backups/.snapshot/foo/wherever you put your database backups using rsnapshot.conf. =cut package rsnapshotDB; =pod =head1 rsnapshotDB.pm =cut use strict; use Data::Dumper; use Cwd 'cwd'; use POSIX qw(strftime); use XML::Validator::Schema; use XML::Simple; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = bless( {}, $class ); my $timestamp = localtime; my %data = ref($_[0]) eq 'HASH' ? %{$_[0]} : (@_); $self->_dbpasswd($data{'dbpasswd'}); $self->_xsd($data{'xsd'}); $self->_tmpDir($data{'tmpDir'}); $self->_verbose($data{'verbose'}); $self->_dbApp($data{'dbApp'}); $self->v("\n\nSTART TIME: $timestamp", 0); return $self; } sub validateXML { my $self = shift; my $xml = $self->_dbpasswd; my $xsd = $self->_xsd; my $validator = XML::Validator::Schema->new(file=> $xsd); my $parser = XML::SAX::ParserFactory->parser(Handler => $validator); $self->v("WAITING: validating xml...", 1); eval { $parser->parse_uri($xml) }; die "File failed validation: $@" if $@; $self->v("FINISH: validated '$xml' against '$xsd'.", 1); return $self; } =pod =head1 C Utitility to parse our XML file for values =cut sub parseXML { my $self = shift; #start xml parsing of conf file. my $xml = $self->_dbpasswd(); my $parser = XML::Simple->new(); #hardcoded xml tag names my $xmlRef = $parser->XMLin($xml, ForceArray => ['hostGroup', 'hostPair', 'databaseHost', 'defaultSSHHost'] ); #count hostGroup tags my $hostGroups = scalar(@{$xmlRef->{'hostGroup'}}); #process hostGroups for (my $i=0; $i<$hostGroups; $i++) { $self->v("START: hostGroup...", 1); #save default hostGroup ssh host my $defaultSSHHost = {}; #process hostPairs foreach my $hostPair (@{$xmlRef->{'hostGroup'}->[$i]->{'hostPair'}}) { $self->v("START: hostPair...", 1); #save databaseHost hashref my $databaseHost = $hostPair->{'databaseHost'}; if ( exists($hostPair->{'defaultSSHHost'}[0]->{'hostType'}) ) { #save default and continue to use it $defaultSSHHost = $hostPair->{'defaultSSHHost'}; } $self->showDB($defaultSSHHost, $databaseHost); $self->v("END: hostPair\n", 1); } $self->v("END: hostGroup\n", 1); } return $self; } =pod =head2 LOGIN REMOTELY: This is the section where you authenticate with the remote ssh server. I'm pretty sure, you can just leave off the password flags if you know what you're doing in the XML Config file rsnapshotDB.conf. Requirement: Net::SSH::Perl. If you don't have root, read about how to install a perl module as an under privileged user (it IS possible - /home/username/modules/). =head2 SHOW DATABASES: C<$self-\>showDB();> This should pull down the list of your database user's databases from the XML configuration file. Note: This is done on the remote SSH server with db access. Since we're not writing or reading there isn't a lock on the table. The one restriction here is that you can actually access your database server remotely from an internal ssh server via ssh tunneling. =head2 PATH: Make sure your prompt binary (ie - mysql) and dumper binary (ie - mysqldump) are in your default path for the ssh user. =cut sub showDB { my ($self, $sshHost, $dbHost) = @_; #ssh my $user = $sshHost->[0]->{'username'}; my $host = $sshHost->[0]->{'hostname'}; #db my $dbApp = $self->_dbApp(); my $dbType = $dbHost->[0]->{'dbType'}; my $dbuser = $dbHost->[0]->{'dbusername'}; my $dbpass = $dbHost->[0]->{'dbpassword'}; my $dbhost = $dbHost->[0]->{'dbhostname'}; #add dbApp binaries to you PATH on the server(s) my $dumper = $dbApp->{$dbType}->{'dumper'}->{'bin'}; my $prompt = $dbApp->{$dbType}->{'prompt'}->{'bin'}; my $dbNames = []; #results from SHOW DATABASES; my $dbpass_arg = defined($dbpass) ? "$dbApp->{$dbType}->{'prompt'}->{'pass'}$dbpass" : ''; #dbpass not required $self->v("START: showDB command...", 1); my $cmdShowDB = "ssh $sshOption $user\@$host \"echo -n 'SHOW DATABASES;' | \ $dbApp->{$dbType}->{'prompt'}->{'bin'} \ $dbApp->{$dbType}->{'prompt'}->{'opts'} \ $dbApp->{$dbType}->{'prompt'}->{'user'} $dbuser \ $dbpass_arg \ $dbApp->{$dbType}->{'prompt'}->{'host'} $dbhost\""; my $out = qx/$cmdShowDB/ or warn 'SHOW DATABASES failed...'; $self->v("CMD: $cmdShowDB -> $out.", 2); $self->v("DONE: showDB command.", 1); #fetch results from query push(@{$dbNames}, split(/\n/, $out)); $self->v(Dumper($dbNames), 2); $self->dumbDB($sshHost, $dbHost, $dbNames); return $self; } =pod =head2 DUMP DATABASE: This is the bulk of the app, via ssh tunneling, logs in to an internal ssh server with access to the database server. The main reason for speeding this application up was becauase a remote database pull is extremely inefficient directly over the internet. The idea here is to use ssh-keygen from this account to your remote ssh server, then do the database dump, and secure copy ('man scp') the file back here locally. The gained result here should be seconds vs. minutes. =cut sub dumbDB { my ($self, $sshHost, $dbHost, $dbNames) = @_; my $user = $sshHost->[0]->{'username'}; my $host = $sshHost->[0]->{'hostname'}; #db my $dbApp = $self->_dbApp(); my $dbType = $dbHost->[0]->{'dbType'}; my $dbuser = $dbHost->[0]->{'dbusername'}; my $dbpass = $dbHost->[0]->{'dbpassword'}; my $dbhost = $dbHost->[0]->{'dbhostname'}; #add dbApp binaries to you PATH on the server(s) my $dumper = $dbApp->{$dbType}->{'dumper'}->{'bin'}; my $prompt = $dbApp->{$dbType}->{'prompt'}->{'bin'}; my $tmpDir = $self->_tmpDir(); #remote tmp directory path my $localTmpDir = cwd(); #need by rsnapshot my $cmdRemoteTmpDir = "ssh $sshOption $user\@$host 'echo -n $tmpDir'"; $self->v("CMD: remote tmp dir '$cmdRemoteTmpDir'.", 2); my $remoteTmpDir = qx/$cmdRemoteTmpDir/ or warn "REMOTE TMP DIR failed..."; $self->v("SET: remote temp dir... '$remoteTmpDir'", 1); #dumper arguments my $dumpOptsArg = $dbApp->{$dbType}->{'dumper'}->{'opts'}; my $dumpHostArg = $dbApp->{$dbType}->{'dumper'}->{'host'}; my $dumpUserArg = $dbApp->{$dbType}->{'dumper'}->{'user'}; my $dumpPassArg = $dbApp->{$dbType}->{'dumper'}->{'pass'}; foreach my $dbName (@{$dbNames}) { my $ftimestamp = strftime "%F-%H.%M", localtime; $self->v("FTIMESTAMP: $ftimestamp", 1); my $file = join('--', $dbType, $dbhost, $dbName, $ftimestamp); my $cmdTestRTD = "ssh $sshOption $user\@$host 'test -d $remoteTmpDir'"; $self->v("CMD: $cmdTestRTD.", 2); my $out = qx/$cmdTestRTD/; if ($?) { $self->v("FAIL: $cmdTestRTD, $out.", 0); my $cmdCreateRTD = "ssh $sshOption TCPKeepAlive $user\@$host 'mkdir -m 0700 $remoteTmpDir'"; my $out = qx/$cmdCreateRTD/; $self->v("CMD: $cmdCreateRTD.", 2); $self->v("FAIL: $cmdCreateRTD, $out", 0) if $?; } else { my $cmdChmodRTD = "ssh $sshOption $user\@$host 'chmod 0700 $remoteTmpDir'"; my $out = qx/$cmdChmodRTD/; $self->v("FAIL: $cmdChmodRTD, $out.", 2) if $?; } #the actual .sql.gz remote file creation! my $cmdRemoteDump = "ssh $sshOption $user\@$host 'umask 0077;nice --adjustment=$niceness $dumper \ $dumpOptsArg $dumpUserArg $dbuser $dumpPassArg" . "$dbpass $dumpHostArg $dbhost \ $dbName > $remoteTmpDir/$file.sql'"; $self->v("WAITING: remote dump...", 1); $out = qx/$cmdRemoteDump/; $self->v("FAIL: $cmdRemoteDump, $out.", 0) if $?; $self->v("CMD: $cmdRemoteDump", 2); $self->v("FINISH: remote dump.", 1); my $cmdRemoteGZip = "ssh $sshOption $user\@$host 'nice --adjustment=$niceness gzip --fast $remoteTmpDir/$file.sql'"; $self->v("WAITING: remote gzip...", 1); $self->v("CMD: $cmdRemoteGZip", 2); $out = qx/$cmdRemoteGZip/; $self->v("FAIL: $cmdRemoteGZip, $out.", 0) if $?; $self->v("FINISH: remote gzip.", 1); my $cmdRemoteSCP = "scp $user\@$host:$remoteTmpDir/$file.sql.gz $localTmpDir"; $self->v("WAITING: remote scp...", 1); $self->v("CMD: $cmdRemoteSCP", 2); $out = qx/$cmdRemoteSCP/; $self->v("FAIL: $cmdRemoteSCP, $out.", 0) if $?; $self->v("FINISH: remote scp.", 1); my $cmdRemoteRM = "ssh $sshOption $user\@$host 'nice --adjustment=$niceness rm $remoteTmpDir/$file.sql.gz'"; $self->v("WAITING: remote remove...", 1); $self->v("CMD: $cmdRemoteRM", 2); $out = qx/$cmdRemoteRM/; $self->v("FAIL: $cmdRemoteRM, $out.", 0) if $?; $self->v("FINISH: remote remove.", 1); } } =pod =head2 SECURE COPY: At this point it's necessary to use ssh-keygen to connect to the server, the local command is using SSH tunneling. ARCHIVING: The move from $localTmpDir to '/backups/.snapshot/database' is determined in rsnapshot.conf and backup rsnapshotDB.pl option (see 'man rsnapshot' for script usage). VERBOSITY: Typically, you would first want to test rsnapshotDB with verbosity set to 1 in the rsnapshotdb.pl see:$verbose => 1. You can increase verbosity ie - 2 instead of 1. Typically, this will dump commands that are being execute remotely and/or locally. LOG FILE: /var/log/rsnapshotDB =cut sub v { my ($self, $msg, $level) = @_; open(LOG, ">>/var/log/rsnapshotDB") or warn "$!"; chmod 0600, "/var/log/rsnapshotDB"; if ($self->_verbose >= $level) { print LOG "$msg\n"; } close(LOG); return $self; } #Class::Accessors simulation sub _dbpasswd { my $self = shift; if (@_ == 0) { return $self->{'dbpasswd'}; } $self->{'dbpasswd'} = shift; return $self->{'dbpasswd'}; } sub _binPath { my $self = shift; if (@_ == 0) { return $self->{'binPath'}; } $self->{'_binPath'} = shift; return $self->{'_binPath'}; } sub _xsd { my $self = shift; if (@_ == 0) { return $self->{'xsd'}; } $self->{'xsd'} = shift; return $self->{'xsd'}; } sub _dbApp { my $self = shift; if (@_ == 0) { return $self->{'dbApp'}; } $self->{'dbApp'} = shift; return $self->{'dbApp'}; } sub _tmpDir { my $self = shift; if (@_ == 0) { return $self->{'tmpDir'}; } $self->{'tmpDir'} = shift; return $self->{'tmpDir'}; } sub _verbose { my $self = shift; if (@_ == 0) { return $self->{'verbose'}; } $self->{'verbose'} = shift; return $self->{'verbose'}; } 1; =pod =head1 MORE INFO: see README.txt =cut rsnapshot-1.4.5/utils/rsnapshotdb/rsnapshotDB.xsd000066400000000000000000000046511435251070200222050ustar00rootroot00000000000000 test valid foo.xml The root node definition Primary use is for segregating hostPair definitions Used for pairing ssh host inside LAN to pull database backup from db server host Default ssh host to connect to in order to pull database backups. defaultSSHHost can be defined once database server from which you are pulling database backups. See defaultSSHHostType for pull location. rsnapshot-1.4.5/utils/rsnaptar000077500000000000000000000037411435251070200164660ustar00rootroot00000000000000#!/bin/sh ############################################################################## # rsnaptar # by Nathan Rosenquist # # A quick hack of a shell script to tar up backup points from the rsnapshot # snapshot root. Sends an e-mail to an address specified on the command line # when finished. # # I set this up in cron to run once a week, take the tar files, # and make DVD-RW backups of the latest snapshot. Your mileage may vary. # # http://www.rsnapshot.org/ ############################################################################## # $Id: rsnaptar,v 1.11 2007/03/22 02:50:21 drhyde Exp $ umask 0077 # DIRECTORIES TAR_DIR="/var/dvd_backup" SNAPSHOT_DIR="/backup/private/snapshots/daily.0" # SHELL COMMANDS LS="/bin/ls" TAR="/bin/tar" CAT="/bin/cat" CHMOD="/bin/chmod" CHOWN="/bin/chown" MKDIR="/bin/mkdir" SENDMAIL="/usr/lib/sendmail -t -oi" HOSTNAME=`/bin/hostname` DATE=`/bin/date +%Y-%m-%d` # uncomment this to gpg encrypt files # the e-mail address the notification is being sent to must have their GPG key # in the public keyring of the user running this backup # GPG="/usr/bin/gpg" # GET E-MAIL ADDRESS if [ ! $1 ]; then echo "Usage: rsnaptar user@domain.com" exit 1 else TO_EMAIL=$1 fi # MAKE ONE TAR FILE FOR EACH BACKUP POINT ${MKDIR} -p ${TAR_DIR}/${DATE}/ cd ${SNAPSHOT_DIR} for BACKUP_POINT in `${LS} ${SNAPSHOT_DIR}`; do # GPG encrypt backups if $GPG is defined if test ${GPG}; then ${TAR} --numeric-owner -cf - ${BACKUP_POINT}/ | \ $GPG --encrypt -r $TO_EMAIL > ${TAR_DIR}/${DATE}/${BACKUP_POINT}.tar.gpg # just create regular tar files else ${TAR} -czf ${TAR_DIR}/${DATE}/${BACKUP_POINT}.tar.gz ${BACKUP_POINT}/ fi done cd - # there are probably sensitive files here, so use the strictest permissions ${CHMOD} -R 0600 ${TAR_DIR}/${DATE}/* ${CHMOD} 0700 ${TAR_DIR}/${DATE}/ ${CAT} < # http://www.rsnapshot.org/ # # This is the script used to semi-automatically GPG sign rsnapshot releases ############################################################################## # $Id: sign_packages.sh,v 1.3 2005/04/02 07:37:07 scubaninja Exp $ for file in `/bin/ls *.tar.gz *.deb *.rpm | grep -v latest`; do # MD5 if [ ! -e "$file.md5" ]; then md5sum $file > $file.md5; echo "Created MD5 Hash for $file"; fi # SHA1 if [ ! -e "$file.sha1" ]; then sha1sum $file > $file.sha1; echo "Created SHA1 hash for $file"; fi # PGP if [ ! -e "$file.asc" ]; then gpg --armor --detach-sign $file; echo "Created PGP Signature for $file"; fi done