├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conf ├── clonepi.conf └── raspbian.excludes ├── dev ├── README.md └── hook-tests │ ├── post-sync-error.sh │ ├── post-sync-info.sh │ ├── post-sync-success.sh │ ├── pre-sync-error.sh │ ├── pre-sync-info.sh │ └── pre-sync-success.sh ├── install.sh ├── src └── clonepi ├── uninstall.sh └── version.txt /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.7.4 (22nd December 2018) 2 | 3 | FEATURES: 4 | 5 | IMPROVEMENTS: 6 | 7 | BUG FIXES: 8 | 9 | - fixed installer when using downloaded release zip bundle 10 | 11 | 12 | ## 1.7.3 (26th November 2018) 13 | 14 | FEATURES: 15 | 16 | IMPROVEMENTS: 17 | 18 | - added -H -A -X options to rsync to preserve additional file attributes 19 | 20 | BUG FIXES: 21 | 22 | 23 | ## 1.7.2 (24th November 2018) 24 | 25 | FEATURES: 26 | 27 | IMPROVEMENTS: 28 | 29 | BUG FIXES: 30 | 31 | - more robust service check 32 | 33 | 34 | ## 1.7.1 (24th November 2018) 35 | 36 | FEATURES: 37 | 38 | IMPROVEMENTS: 39 | 40 | - added check for udisks2 in pre-flight checks as known to cause issues 41 | - clone process timer now includes time spent doing cleanup 42 | 43 | BUG FIXES: 44 | 45 | 46 | ## 1.7.0 (24th November 2018) 47 | 48 | FEATURES: 49 | 50 | - added --services switch to automatically stop + start systemctl services 51 | 52 | IMPROVEMENTS: 53 | 54 | BUG FIXES: 55 | 56 | - fixed pre-sync hook not being called if the dest disk needs initialising 57 | 58 | 59 | ## 1.6.2 (19th July 2018) 60 | 61 | FEATURES: 62 | 63 | IMPROVEMENTS: 64 | 65 | - made installer more self-aware 66 | 67 | BUG FIXES: 68 | 69 | - fixed bug where re-cloning loop device immediately after finishing previous clone could result in file not found error 70 | 71 | 72 | ## 1.6.1 (19th July 2018) 73 | 74 | FEATURES: 75 | 76 | - optional trim of source disk for optimum compressed file output size 77 | 78 | IMPROVEMENTS: 79 | 80 | - installer exit on update, to allow for updates to itself 81 | - run output 82 | - documentation 83 | 84 | BUG FIXES: 85 | 86 | - fixed empty clone image file created even if not confirmed by user 87 | 88 | 89 | ## 1.6.0 (17th July 2018) 90 | 91 | FEATURES: 92 | 93 | - optional gzip compression on clone to file 94 | 95 | IMPROVEMENTS: 96 | 97 | - moved clone to file init to main clone process so it can be confirmed 98 | - hook-pre-sync & hook-post-sync switch params can be seperated by space to allows autopath completion 99 | - documentation 100 | - dev helpers 101 | 102 | BUG FIXES: 103 | 104 | 105 | ## 1.5.3 (14th July 2018) 106 | 107 | FEATURES: 108 | 109 | IMPROVEMENTS: 110 | 111 | - formatting on help 112 | 113 | BUG FIXES: 114 | 115 | 116 | ## 1.5.2 (14th July 2018) 117 | 118 | FEATURES: 119 | 120 | - installer now checks config files versions and backs-up + updates them as necessary 121 | 122 | IMPROVEMENTS: 123 | 124 | - installer outputs commands for installing missing dependencies 125 | - README 126 | 127 | BUG FIXES: 128 | 129 | 130 | ## 1.5.1 (14th July 2018) 131 | 132 | FEATURES: 133 | 134 | IMPROVEMENTS: 135 | 136 | - show loop device deletion command on associated warning 137 | 138 | BUG FIXES: 139 | 140 | - stop hook checks throwing error 141 | 142 | 143 | ## 1.5.0 (8th July 2018) 144 | 145 | FEATURES: 146 | 147 | IMPROVEMENTS: 148 | 149 | - moved pre & post sync hooks to switches, to allow for more flexible use (potential breaking change) 150 | 151 | BUG FIXES: 152 | 153 | 154 | ## 1.4.3 (7th July 2018) 155 | 156 | FEATURES: 157 | 158 | IMPROVEMENTS: 159 | 160 | - include device/file being cloned to in --script mode output - v useful for cron logs! 161 | 162 | BUG FIXES: 163 | 164 | - README typo in --ignore-warnings switch name 165 | 166 | 167 | ## 1.4.2 (7th July 2018) 168 | 169 | FEATURES: 170 | 171 | IMPROVEMENTS: 172 | 173 | - documentation 174 | - run output 175 | 176 | BUG FIXES: 177 | 178 | 179 | ## 1.4.1 (7th July 2018) 180 | 181 | FEATURES: 182 | 183 | - quiet mode, reduce run output to important messages & errors only 184 | 185 | IMPROVEMENTS: 186 | 187 | - run output 188 | 189 | BUG FIXES: 190 | 191 | - occasional fsyncing i/o errors when querying file image 192 | 193 | 194 | ## 1.4.0 (7th July 2018) 195 | 196 | FEATURES: 197 | 198 | - clone to file 199 | 200 | IMPROVEMENTS: 201 | 202 | - run output 203 | - warning handling 204 | 205 | BUG FIXES: 206 | 207 | - timestamp at end of run changed for consistency 208 | 209 | 210 | ## 1.3.1 (4th July 2018) 211 | 212 | FEATURES: 213 | 214 | 215 | IMPROVEMENTS: 216 | 217 | - further refinement to EXPORT_PATH handling 218 | 219 | BUG FIXES: 220 | 221 | 222 | ## 1.3.0 (4th July 2018) 223 | 224 | FEATURES: 225 | 226 | - added --version switch 227 | 228 | IMPROVEMENTS: 229 | 230 | - moved WAIT_BEFORE_UNMOUNT from config to --wait-before-unmount switch as more convenient (potential breaking change) 231 | - additional check that PATH is not set before setting it via EXPORT_PATH, enable it by default in conf (potential breaking change) 232 | - removed most shorthand switches because they are potentially confusing & therefore dangerous (they were undocumented for this reason and therefore this is not considered a potential breaking change) 233 | - documentation 234 | 235 | BUG FIXES: 236 | 237 | 238 | ## 1.2.2 (4th July 2018) 239 | 240 | FEATURES: 241 | 242 | 243 | IMPROVEMENTS: 244 | 245 | 246 | BUG FIXES: 247 | 248 | - added missed conf var 249 | 250 | 251 | ## 1.2.1 (4th July 2018) 252 | 253 | FEATURES: 254 | 255 | - added EXPORT_PATH config var - makes running from cron easy 256 | 257 | IMPROVEMENTS: 258 | 259 | 260 | BUG FIXES: 261 | 262 | 263 | ## 1.2.0 (3rd July 2018) 264 | 265 | FEATURES: 266 | 267 | 268 | IMPROVEMENTS: 269 | 270 | - moved EXIT_ON_WARNING from config to --ignore-warnings switch as more convenient (potential breaking change) 271 | - run output 272 | - documentation 273 | - reversed CHANGELOG order to list latest first 274 | 275 | BUG FIXES: 276 | 277 | - removed erroneous exit 278 | 279 | 280 | ## 1.1.0 (3rd July 2018) 281 | 282 | FEATURES: 283 | 284 | - added --script mode 285 | 286 | IMPROVEMENTS: 287 | 288 | - changed rsync switch names to avoid confusion as to their purpose (potential breaking change) 289 | - documentation 290 | 291 | BUG FIXES: 292 | 293 | - fixed silly typo in run output 294 | 295 | 296 | ## 1.0.3 (2nd July 2018) 297 | 298 | FEATURES: 299 | 300 | - UUID device lookup 301 | 302 | IMPROVEMENTS: 303 | 304 | - documentation 305 | 306 | BUG FIXES: 307 | 308 | 309 | 310 | ## 1.0.2 (18th June 2017) 311 | 312 | FEATURES: 313 | 314 | - Initial release 315 | 316 | IMPROVEMENTS: 317 | 318 | 319 | BUG FIXES: 320 | 321 | 322 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions and pull requests are welcome, but we ask the following guidelines are respected; 4 | 5 | + the PR adds a compelling new feature 6 | + OR addresses a known / open issue 7 | + OR improves reliability / robustness 8 | + adhere to current coding style used within the source - good comments, tab indents etc. 9 | + be hygenic, ensure all secondary tasks are done - robustness checks, update summary, readme, installer etc. 10 | + don't update version num - will be incremented when the PR is merged 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Paul Fernihough 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClonePi 2 | 3 | ClonePi will clone a running Raspberry Pi to a destination SD card, device or a file. Features... 4 | 5 | + Works with standard 2 partition Raspbian setups, multi-partition NOOBS setups and more 6 | + Incremental on-the-fly cloning 7 | + Clone to a physical device and optionally size up or down to fit the destination disk 8 | + Clone to a file (stored on a NAS or external disk) and optionally compress it 9 | + Optional unattended operation, suitable for cron use 10 | + Configuration options allow it to be tuned to work with many systems 11 | + Script hooks allow it to be extended beyond the default use cases 12 | + Headless operation, works without a GUI 13 | 14 | Raspbian is tested - may be tweaked to work with others systems. 15 | 16 | ## Installing / Updating / Removing 17 | 18 | Clone this repo to your Raspberry Pi (or download the zip). Run the installer as root 19 | ``` 20 | git clone https://github.com/SpoddyCoder/clonepi.git 21 | cd clonepi 22 | sudo ./install.sh 23 | ``` 24 | 25 | Simply re-run the installer at any time to update to latest version. To completely remove ClonePi and config files, run the uninstaller 26 | ``` 27 | sudo ./uninstall.sh 28 | ``` 29 | A copy of the config files is placed in `/tmp/clonepi-bak/` in case you need to restore them. 30 | 31 | 32 | ## Usage 33 | 34 | ClonePi must be run as root. Pick the drive you want to clone to (destination) and any options you want to apply 35 | ``` 36 | sudo clonepi [device|UUID|file] [options...] 37 | ``` 38 | 39 | ClonePi can clone to a device using standard device identifier 40 | ``` 41 | sudo clonepi /dev/sdb 42 | ``` 43 | 44 | Or to a device using its UUID 45 | ``` 46 | sudo clonepi 5e8e1777-797d-4f59-9696-4a6d42f0690a 47 | ``` 48 | 49 | Or to an image file. This requires as much space as the source disk (place on a NAS or external drive) 50 | ``` 51 | sudo clonepi /mnt/nas/pi-system-backups/my-pi.img 52 | ``` 53 | 54 | When cloning to a file you can optionally compress the output stream with `gzip`. It can be very useful to trim the source disk prior to cloning, as this can significantly reduce the size of the compressed image - ClonePi makes this easy 55 | ``` 56 | sudo clonepi /mnt/nas/pi-system-backups/my-pi.img.gz --init-destination --compress-file --trim-source 57 | ``` 58 | Compressed image files cannot be incrementally cloned to. This will be smaller than the source disk, so *may* fit on the local filsystem if desired. The compression requires a lot of CPU, so this will take much longer than a normal clone. 59 | 60 | 61 | ### Options 62 | 63 | + `--help` or `-h` show usage info 64 | + `--version` or `-v` show ClonePi version 65 | + `--quiet` or `-q` don't show info, show only warnings & errors. 66 | + `--init-destination` force initialisation of the destination disk. This will erase all of its contents. 67 | + `--fill-destination` fill destination disk. Implies `--init-destination`. Will attempt to resize the last partition to fill the destination disk. If the source disk is larger than the destination it will attempt to resize down, but this may or may not leave room for the content. 68 | + `--compress-file` only for cloning to file - will compress the output stream using gzip. Does not apply to device cloning. NB: Incremental cloning isn't possible to a compressed image file. 69 | + `--trim-source` useful when using `--compress-file`. Will run a `fstrim` on all source partitions, which zero's unused space and therefore can significantly reduce the size of the compressed image file. 70 | + `--script` run in non-interactive mode. All user input is assumed to be yes. Useful for running via cron. You are strongly advised to test your clone run a few times before automating the process. 71 | + `--services` comma separated list of systemctl managed services to stop before clone process starts & re-started after finishing. 72 | + `--ignore-warnings` dont abort when a warning is hit. ClonePi performs a number of checks before starting a run & outputs a warning if it thinks you may have something wrong. It then then aborts for safety. Due to the destructive nature of what it does, you should use this switch with caution. When applied along with the `--script` switch, this is especially dangerous. 73 | + `--wait-before-unmount` pause at the end of a clone run, before the destination is unmounted. Useful for making changes to clone before using it in another Pi. 74 | + `--hook-pre-sync` specify a script to be run prior to main sync process. See script hooks section for more detail. 75 | + `--hook-post-sync` specify a script to be run post the main sync process, but before clone is unmounted 76 | + `--rsync-verbose` list all files as they are rsynced. 77 | + `--rsync-dry-run` apply --dry-run flag to rsync, which will show files that would be synced, but not actually sync them. 78 | 79 | ### Modes of Operation 80 | 81 | + If the destination disk/file matches the source partition structure then ClonePi assumes this is an initialised disk and an incremental copy will be performed. 82 | + ClonePi will require an initialising of the destination disk/file if its partition structure does not match the source disk 83 | + This behavior can be changed with the switches. 84 | 85 | #### Initialisation + Copy 86 | First time setup of the destination disk/file. 87 | It will format the destination card/file to match the source disk partition structure and then perform a first time sync. 88 | This can be expected to take a while. 89 | Example: 90 | ``` 91 | sudo clonepi /dev/sdc --init-destination 92 | ``` 93 | 94 | #### Incremental Copy 95 | Subsequent updates to an initialised clone. 96 | It will sync files that have changed since last sync. 97 | This will be much quicker than full init + copy. 98 | Example: 99 | ``` 100 | sudo clonepi /dev/sdc 101 | ``` 102 | 103 | #### Non-interactive Mode 104 | Run ClonePi unattended - all user input is assumed yes. 105 | Useful for automated incremental backup of an initialised disk. 106 | You are advised to use the destination disk UUID, as standard file device identifers can change between reboots. 107 | Example: 108 | ``` 109 | sudo clonepi 5e8e1777-797d-4f59-9696-4a6d42f0690a --script 110 | ``` 111 | 112 | ### Use cases 113 | Typical use cases for ClonePi are backing up a system for disaster recovery or cloning a system to run on other Pi's. 114 | The options, configuration files and script hooks allow for a lot of flexibility. 115 | Some typical example use cases follow 116 | 117 | #### Backup to a plugged in SD card 118 | 119 | 1. Initialise disk + perform first time copy, eg: `sudo clonepi /dev/sdb --init-destination` 120 | 1. Perform incremental update at regular intervals, eg: `sudo clonepi /dev/sdb` 121 | 122 | #### Automatically do incremental backup to a file on a NAS once a week 123 | 124 | 1. Mount NAS to the Pi (see google for instructions, inside `/mnt/` is normal) 125 | 1. Initialise file + perform first time copy, eg: `sudo clonepi /mnt/nas/system-backups/my-pi.img --init-destination` 126 | 1. Perform incremental updates once a week, automatically via cron `sudo nano crontab -e` 127 | 1. Add the following line to run clonepi at midnight on sunday and redirect the output to a log file 128 | ``` 129 | 00 00 * * 0 /usr/local/sbin/clonepi /mnt/nas/system-backups/my-pi.img --script --quiet >> /var/log/clonepi.log 2>&1 130 | ``` 131 | 1. Read the last 50 lines of the log file at any time to ensure its running properly `sudo tail -50 /var/log/clonepi.log` 132 | 133 | #### Cloning SD card for other PI's 134 | 135 | 1. Initialise disk + perform first time copy, eg: `sudo clonepi /dev/sdb --init-destination --wait-before-unmount` 136 | 1. Use the `--wait-before-unmount` switch to pause at the end of the cloning process. Before unmounting the clone disk, use a 2nd shell window to modify any files on the clone you need for it to work on other Pi's 137 | 1. Eg: you may want to edit the hostname, network configuration etc. 138 | 1. **Top Tip**: You can use the script hooks to automate this final step, see below. 139 | 140 | #### Backup to a compressed file for long term storage on external drive 141 | 1. Trim the soruce disk, initialise file and compress output stream, eg: `sudo clonepi /mnt/ext-hd/system-backups/my-pi.img.gz --init-destination --compress-file --trim-source` 142 | 143 | 144 | ## Advanced Configuration 145 | 146 | ### Stopping & Starting Services 147 | It can be useful to stop some services prior to the clone process to ensure consistency of the disk. Clonepi makes this easy for systemctl managed services, eg... 148 | ``` 149 | sudo clonepi /dev/sdb --services=udisks2,plexmediaserver 150 | ``` 151 | This will stop udisks2.service & plexmediaserver.service before starting the clone process and will restart them both after finishing. 152 | 153 | ### Script Hooks 154 | Script hooks allow you to inject your own code at specific points during the ClonePi process, eg... 155 | ``` 156 | sudo clonepi /dev/sdb --hook-pre-sync /home/pi/clonepi-hooks/stop-services.sh --hook-post-sync /home/pi/clonepi-hooks/start-services.sh 157 | ``` 158 | 159 | Currently there are two hooks available 160 | 161 | + `--hook-pre-sync` runs after the clone disk/file has been setup/initialised, but before the main rsync process 162 | + `--hook-post-sync` runs after the main rsync process has finished, but before the clone disk/file is unmounted 163 | 164 | Typical use cases; 165 | 166 | + Stop services/apps before starting sync and restarting them after sync finishes. (NB: the `--services` switch can also do this) 167 | + Prepare the source disk before syncing. 168 | + Prepare the clone disk after sync - eg: modify hostname/fixed IP address if intended for anothe Pi on your network etc. 169 | 170 | All script hooks run as a subprocess so they have access to all ClonePi variables. 171 | You can optionally end your scripts with an exit code and ClonePi will take the following actions (no exit code = success) 172 | 173 | + **exit 0** and clonepi will **continue** 174 | + **exit 1** and clonepi will **output an error and abort** 175 | + **exit 2** and clonepi will **output info and continue** 176 | 177 | ### Config Files 178 | ClonePi utilises configuration files at `/etc/clonepi/`. 179 | These can be edited to tune ClonePi for your system and use case. 180 | Notes on each of the configurable items are included in the files. 181 | 182 | + **clonepi.conf** - main config file 183 | + **raspbian.excludes** - files/directories to be excluded from the running OS sync 184 | 185 | 186 | ## Further Help & Info 187 | 188 | ### Some hints for the less experienced 189 | You will need to know the device name of your SD card. ClonePi cannot determine this for you, but run the following command to identify all attached disks... 190 | ``` 191 | sudo fdisk -l 192 | ``` 193 | A couple of tips and a couple warnings when identifying your device; 194 | 195 | 1. size is normally the easiest way to identify particular SD cards 196 | 1. normally the first disk listed will be the current booted Raspberry Pi SD card: `mmcblk0p1 / mmcblk0p2` - i.e. the disk you will be cloning from 197 | 1. using the incorrect device identifier will likely result in data loss on that disk - check twice 198 | 1. depending on the system setup, the device identifier **can change** between reboots, so you should **always** confirm the correct disk before running ClonePi 199 | 200 | 201 | ### Some hints for the more experienced 202 | 1. If you are running ClonePi via cron, **always use the UUID** to target the drive, see hint above for reason why 203 | 1. To find your device UUID, use: `sudo blkid` 204 | 1. The script hooks are your friend 205 | 1. ClonePi probably works on non-Debian systems, but you may need to modify the OS_EXCLUDES_FILE (please consider contributing) 206 | 1. Config files are good for most setups, but could be tweaked for others (please consider contributing) 207 | 1. The PARTUUID of the boot volume on the source disk will be identical on the clone (the MBR is a bit for bit clone). 208 | 209 | 210 | ## Additional Notes 211 | This project owes it's genesis to rpi-clone and is based on the same clever "partial dd & full rsync" approach. 212 | Check it out @ https://github.com/billw2/rpi-clone 213 | 214 | 215 | ## Authors 216 | 1. **Paul Fernihough** - original author - (paul--at--spoddycoder.com) 217 | -------------------------------------------------------------------------------- /conf/clonepi.conf: -------------------------------------------------------------------------------- 1 | # 2 | # ClonePi configuration file 3 | # 4 | # last updated @ v1.7.3 5 | # 6 | # IMPORTANT NOTE: 7 | # this configuration file defines bash variables used by the clonepi command 8 | # it would be possible to put bash commands in here and they would run when clonepi runs 9 | # do not do this - use the script hooks instead 10 | # 11 | 12 | 13 | # source disk 14 | # ClonePi is intended for Raspberry Pi systems - you shouldn't need to change this 15 | SRC_DISK="/dev/mmcblk0" 16 | 17 | 18 | # directory containing unmounted source disk partition mounts 19 | SRC_MOUNT_DIR="/mnt/source" 20 | 21 | 22 | # directory containing destination disk partition mounts 23 | CLONE_MOUNT_DIR="/mnt/clone" 24 | 25 | 26 | # rsync options - used in all rsync processes 27 | # these have been carefully chosen, think twice before editing 28 | RSYNC_OPTIONS="--force --delete -rltgopxWDEHAX" 29 | 30 | 31 | # rsync excludes file applied to the running OS's root sync process 32 | # if you only need to add excludes, edit the file not this option 33 | OS_EXCLUDES_FILE="/etc/clonepi/raspbian.excludes" 34 | 35 | 36 | # if sbin is not found in PATH, ClonePi will export this as PATH 37 | # useful if running from cron and you don't have a full environment setup 38 | # the defaults here are good for Rapsbian and likely most other linux distros 39 | EXPORT_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 40 | -------------------------------------------------------------------------------- /conf/raspbian.excludes: -------------------------------------------------------------------------------- 1 | # 2 | # Excludes file for Raspbian OS 3 | # last updated @ v1.0.0 4 | # 5 | # defines files/directories to be excluded from the rsync process 6 | # 7 | 8 | # 9 | # standard excludes 10 | # these have been carefully chosen, think twice before editing 11 | # 12 | .gvfs 13 | /dev/* 14 | /proc/* 15 | /run/* 16 | /sys/* 17 | /tmp/* 18 | /var/swap/* 19 | lost\+found/* 20 | 21 | 22 | # 23 | # place user defined excludes below 24 | # system specific, eg: application tmp + cache directories 25 | # NB: no need to define external drives / mount points - clonepi does not cross filesystems by default 26 | # 27 | #/var/www/html/mysite/crazybigcache/* 28 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # ClonePi Development Directory 2 | Contains useful helpers for ClonePi dev 3 | 4 | ## hook tests 5 | For testing script hooks, these output a simple message & one of the 3 exit codes 6 | 7 | ## TODO: build script 8 | 9 | ## TODO: unit tests 10 | -------------------------------------------------------------------------------- /dev/hook-tests/post-sync-error.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the ERROR hook-POST-sync script example!" 4 | exit 1 5 | -------------------------------------------------------------------------------- /dev/hook-tests/post-sync-info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the INFO hook-POST-sync script example!" 4 | exit 2 5 | -------------------------------------------------------------------------------- /dev/hook-tests/post-sync-success.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the SUCCESS hook-POST-sync script example!" 4 | exit 0 5 | -------------------------------------------------------------------------------- /dev/hook-tests/pre-sync-error.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the ERROR hook-PRE-sync script example!" 4 | exit 1 5 | -------------------------------------------------------------------------------- /dev/hook-tests/pre-sync-info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the INFO hook-PRE-sync script example!" 4 | exit 2 5 | -------------------------------------------------------------------------------- /dev/hook-tests/pre-sync-success.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the SUCCESS hook-PRE-sync script example!" 4 | exit 0 5 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install script for ClonePi 4 | # last updated @ v1.7.4 5 | # 6 | # - gets latest available version from github 7 | # - updates source repo if possible 8 | # - checks dependencies: system, rsync and fsck.vfat 9 | # - copies clonepi to /usr/local/sbin & sets owner/permisions 10 | # - copies configuration files to /etc/clonepi/ & sets owner/perms 11 | # 12 | # ClonePi version format: "major.minor.revision" 13 | 14 | 15 | # 16 | # helper functions 17 | # 18 | doMsg() 19 | { 20 | # msg, type 21 | case "$2" in 22 | warn) 23 | printf "WARNING: ${1}\n" 24 | echo 25 | ;; 26 | user-abort) 27 | printf "User aborted: ${1}\n" 28 | echo 29 | exit 0 30 | ;; 31 | error) 32 | printf "ERROR: ${1}\n" 33 | echo "Aborting!" 34 | echo 35 | exit 1 36 | ;; 37 | info-abort) 38 | printf "INFO: ${1}\n" 39 | echo "Nothing to do!" 40 | echo 41 | exit 0 42 | ;; 43 | esac 44 | } 45 | 46 | 47 | # 48 | # config + setup 49 | # 50 | INSTALL_DIR="/usr/local/sbin" 51 | CONF_DIR="/etc/clonepi" 52 | BAK_DIR="/tmp/clonepi-conf-bak" 53 | GITHUB_VERSION_URL="https://raw.githubusercontent.com/SpoddyCoder/clonepi/master/version.txt" 54 | CUR_INSTALLER_VER=`head -10 install.sh | grep "last updated" | cut -f2 -d'@' | xargs | cut -f2 -d'v'` 55 | 56 | echo 57 | echo "Welcome to the ClonePi installer" 58 | echo 59 | if [ `id -u` != 0 ]; then 60 | doMsg "The ClonePi installer needs to be run as root" "error" 61 | fi 62 | # get current state 63 | # ...version for this installer 64 | NEW_VERSION=`cat version.txt | xargs` 65 | # ...version currently installed 66 | CUR_VERSION=0 67 | if [ -f ${INSTALL_DIR}/clonepi ]; then 68 | CUR_VERSION=`${INSTALL_DIR}/clonepi -v | grep "ClonePi v" | sed 's/^ClonePi v//'` 69 | fi 70 | # ...version on github 71 | echo "Checking for latest version number at GitHub..." 72 | REMOTE_VERSION=$(wget -q -O - $GITHUB_VERSION_URL) 73 | if [ $? = 0 ]; then 74 | echo "...latest available version is ${REMOTE_VERSION}" 75 | else 76 | echo "...error trying to get latest version - assuming source repo/dir is upto date." 77 | REMOTE_VERSION=$NEW_VERSION 78 | fi 79 | # ...current conf & excludes versions, if installed 80 | CUR_CONF_VER="" 81 | CUR_EXCLUDES_VER="" 82 | if [ -d $CONF_DIR ]; then 83 | if [ -f ${CONF_DIR}/clonepi.conf ]; then 84 | CUR_CONF_VER=`head -5 ${CONF_DIR}/clonepi.conf | grep "last updated" | cut -f2 -d'@' | xargs | cut -f2 -d'v'` 85 | fi 86 | if [ -f ${CONF_DIR}/raspbian.excludes ]; then 87 | CUR_EXCLUDES_VER=`head -5 ${CONF_DIR}/raspbian.excludes | grep "last updated" | cut -f2 -d'@' | xargs | cut -f2 -d'v'` 88 | fi 89 | fi 90 | 91 | 92 | # 93 | # update repo to latest, if possible 94 | # 95 | if [ -d .git ]; then 96 | # running inside git repo 97 | if [ "$REMOTE_VERSION" = "$NEW_VERSION" ]; then 98 | echo "Source repo is at latest version." 99 | echo 100 | else 101 | doMsg "Source repo is not upto date." "warn" 102 | read -p "Perform a 'git pull origin master' now (yes|no)? " UI < /dev/tty 103 | if [ ! "$UI" = "y" -a ! "$UI" = "yes" ]; then 104 | echo "Continuing without updating the repo" 105 | echo 106 | else 107 | echo "Updating repo..." 108 | su - `logname` -c "cd `pwd` && git pull origin master" 109 | if [ $? = 0 ]; then 110 | echo "Repo updated sucessfully" 111 | # check if installer has been updated 112 | NEW_INSTALLER_VER=`head -5 install.sh | grep "last updated" | cut -f2 -d'@' | xargs | cut -f2 -d'v'` 113 | if [ "$CUR_INSTALLER_VER" != "$NEW_INSTALLER_VER" ]; then 114 | doMsg "the installer has been updated, please re-run" "error" 115 | fi 116 | else 117 | doMsg "problem updating repo." "error" 118 | fi 119 | NEW_VERSION=`cat version.txt | xargs` 120 | fi 121 | fi 122 | else 123 | # not a git repo, assume download zip 124 | if [ "$REMOTE_VERSION" != "$NEW_VERSION" ]; then 125 | doMsg "Source install dir is not upto date.\nDownload the latest zip to get latest version." "error" 126 | fi 127 | fi 128 | 129 | 130 | # 131 | # check dependencies 132 | # 133 | # system check 134 | IS_RASPBIAN=`lsb_release -d | grep -i Raspbian` 135 | if [ -z "$IS_RASPBIAN" -o ! $? = 0 ]; then 136 | doMsg "this doesn't look like a Rapsbian system." "warn" 137 | echo "Your OS is reported as:" 138 | lsb_release -d 139 | echo 140 | echo "ClonePi is designed to work on Raspberry Pi systems. Please proceed, if you know what you're doing." 141 | echo 142 | fi 143 | # rsync check 144 | if ! rsync --version > /dev/null; then 145 | doMsg "ClonePi requires rsync. Run the following to install: sudo apt-get update && sudo apt-get install rsync" "error" 146 | fi 147 | # fsck.vfat check 148 | if ! test -e /sbin/fsck.vfat; then 149 | doMsg "ClonePi requires dosfstools. Run the following to install: sudo apt-get update && sudo apt-get install dosfstools" "error" 150 | fi 151 | 152 | # 153 | # pre-install setup 154 | # 155 | NEW_CONF_VER=`head -5 conf/clonepi.conf | grep "last updated" | cut -f2 -d'@' | xargs | cut -f2 -d'v'` 156 | NEW_EXCLUDES_VER=`head -5 conf/raspbian.excludes | grep "last updated" | cut -f2 -d'@' | xargs | cut -f2 -d'v'` 157 | UPGRADE_CONF=false 158 | UPGRADE_EXCLUDES=false 159 | INSTALL_CLONEPI=true 160 | INSTALL_CONF_DIR=true 161 | INSTALL_CONF_FILE=true 162 | INSTALL_EXCLUDES_FILE=true 163 | if [ "$CUR_VERSION" = "$NEW_VERSION" ]; then 164 | INSTALL_CLONEPI=false 165 | fi 166 | if [ -d ${CONF_DIR} ]; then 167 | INSTALL_CONF_DIR=false 168 | if [ -f ${CONF_DIR}/clonepi.conf ]; then 169 | INSTALL_CONF_FILE=false 170 | fi 171 | if [ -f ${CONF_DIR}/raspbian.excludes ]; then 172 | INSTALL_EXCLUDES_FILE=false 173 | fi 174 | fi 175 | if ! $INSTALL_CONF_FILE; then 176 | # check if existing version is latest 177 | if [ "$CUR_CONF_VER" = "$NEW_CONF_VER" ]; then 178 | UPGRADE_CONF=false 179 | else 180 | UPGRADE_CONF=true 181 | fi 182 | fi 183 | if ! $INSTALL_EXCLUDES_FILE; then 184 | # check if existing version is latest 185 | if [ "$CUR_EXCLUDES_VER" = "$NEW_EXCLUDES_VER" ]; then 186 | UPGRADE_EXCLUDES=false 187 | else 188 | UPGRADE_EXCLUDES=true 189 | fi 190 | fi 191 | 192 | # 193 | # summarise and get user confirmation 194 | # 195 | if [ "$INSTALL_CONF_DIR" = false -a "$INSTALL_CONF_FILE" = false -a "$INSTALL_EXCLUDES_FILE" = false -a "$INSTALL_CLONEPI" = false -a "$UPGRADE_CONF" = false -a "$UPGRADE_EXCLUDES" = false ]; then 196 | doMsg "ClonePi v${CUR_VERSION} already installed with latest config files" "info-abort" 197 | else 198 | echo "This will..." 199 | if $INSTALL_CLONEPI; then 200 | if [ "$CUR_VERSION" = 0 ]; then 201 | echo " - Install ClonePi $NEW_VERSION" 202 | else 203 | echo " - Update ClonePi from $CUR_VERSION to $NEW_VERSION" 204 | fi 205 | fi 206 | if $INSTALL_CONF_DIR; then 207 | echo " - Install missing config directory at ${CONF_DIR}" 208 | fi 209 | if $INSTALL_CONF_FILE; then 210 | echo " - Install missing config file at ${CONF_DIR}/clonepi.conf" 211 | fi 212 | if $INSTALL_EXCLUDES_FILE; then 213 | echo " - Install missing config file at ${CONF_DIR}/raspbian.excludes" 214 | fi 215 | if $UPGRADE_CONF; then 216 | echo " - Upgrade the config file at ${CONF_DIR}/clonepi.conf" 217 | fi 218 | if $UPGRADE_EXCLUDES; then 219 | echo " - Upgrade the excludes file at ${CONF_DIR}/raspbian.excludes" 220 | fi 221 | fi 222 | echo 223 | read -p "Continue with install (yes|no)? " UI < /dev/tty 224 | if [ ! "$UI" = "y" -a ! "$UI" = "yes" ]; then 225 | doMsg "installation not confirmed" "user-abort" 226 | fi 227 | 228 | # 229 | # And finally install 230 | # 231 | if $INSTALL_CLONEPI; then 232 | rm -f ${INSTALL_DIR}/clonepi && cp ./src/clonepi ${INSTALL_DIR}/clonepi && chown root:root ${INSTALL_DIR}/clonepi && chmod u+x ${INSTALL_DIR}/clonepi 233 | if [ "$?" = 0 ]; then 234 | echo "Installed ClonePi to ${INSTALL_DIR}/clonepi" 235 | else 236 | doMsg "could not install ClonePi to ${INSTALL_DIR}/clonepi" "error" 237 | fi 238 | fi 239 | 240 | if $INSTALL_CONF_DIR; then 241 | mkdir ${CONF_DIR} 242 | if [ "$?" = 0 ]; then 243 | echo "Created config directory at ${CONF_DIR}" 244 | else 245 | doMsg "could not create config directory at ${CONF_DIR}" "error" 246 | fi 247 | fi 248 | 249 | if $INSTALL_CONF_FILE; then 250 | cp ./conf/clonepi.conf ${CONF_DIR}/clonepi.conf 251 | if [ "$?" = 0 ]; then 252 | echo "Installed config file at ${CONF_DIR}/clonepi.conf" 253 | else 254 | doMsg "could not install config file at ${CONF_DIR}/clonepi.conf" "error" 255 | fi 256 | fi 257 | 258 | if $INSTALL_EXCLUDES_FILE; then 259 | cp ./conf/raspbian.excludes ${CONF_DIR}/raspbian.excludes 260 | if [ "$?" = 0 ]; then 261 | echo "Installed config file at ${CONF_DIR}/raspbian.excludes" 262 | else 263 | doMsg "could not install config file at ${CONF_DIR}/raspbian.excludes" "error" 264 | fi 265 | fi 266 | 267 | if [ "$UPGRADE_CONF" = true -o "$UPGRADE_EXCLUDES" = true ]; then 268 | rm -rf ${BAK_DIR} && mkdir ${BAK_DIR} 269 | if $UPGRADE_CONF; then 270 | mv ${CONF_DIR}/clonepi.conf ${BAK_DIR} 271 | cp ./conf/clonepi.conf ${CONF_DIR}/clonepi.conf 272 | if [ "$?" = 0 ]; then 273 | echo "Replaced config file at ${CONF_DIR}/clonepi.conf with latest version" 274 | doMsg "Your old config file has been moved to ${BAK_DIR} - if you have modified this, you may need to merge back in any of your own changes" "warn" 275 | else 276 | doMsg "could not install config file at ${CONF_DIR}/clonepi.conf" "error" 277 | fi 278 | fi 279 | if $UPGRADE_EXCLUDES; then 280 | mv ${CONF_DIR}/raspbian.excludes ${BAK_DIR} 281 | cp ./conf/raspbian.excludes ${CONF_DIR}/raspbian.excludes 282 | if [ "$?" = 0 ]; then 283 | echo "Replaced excludes file at ${CONF_DIR}/raspbian.excludes with latest version" 284 | doMsg "Your old excludes file has been moved to ${BAK_DIR} - if you have modified this, you may need to merge back in your own changes" "warn" 285 | else 286 | doMsg "could not install excludes file at ${CONF_DIR}/raspbian.excludes" "error" 287 | fi 288 | fi 289 | fi 290 | 291 | chown -R root:root ${CONF_DIR} 292 | chmod -R 755 ${CONF_DIR} 293 | echo 294 | echo "Installation complete!" 295 | echo 296 | exit 0 297 | -------------------------------------------------------------------------------- /src/clonepi: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # 5 | # see CHANGELOG.txt for release notes 6 | # 7 | VER=1.7.4 8 | 9 | 10 | 11 | ################################################# 12 | # helper functions 13 | ################################################# 14 | 15 | usageInfo() 16 | { 17 | echo "ClonePi v${VER}" 18 | if [ "$1" = "version-only" ]; then 19 | return 20 | fi 21 | echo "https://github.com/SpoddyCoder/clonepi/" 22 | echo 23 | echo "Usage:" 24 | echo " sudo clonepi [device|uuid|file] [options...]" 25 | echo 26 | echo "Options:" 27 | echo " --help|-h usage info" 28 | echo " --version|-v version info" 29 | echo " --quiet|-q only output warnings and errors" 30 | echo " --init-destination force initialisation of the destination disk" 31 | echo " --fill-destination resize last partition on destination to fill disk" 32 | echo " --compress-file gzip resulting image file (n/a when cloning to device)" 33 | echo " --trim-source trim source disk to optimize the compressed image file size" 34 | echo " --script run in non-interactive mode" 35 | echo " --services list of systemctl services to stop pre-sync & start post-sync" 36 | echo " --wait-before-unmount pause after the clone has finished, but before it's unmounted" 37 | echo " --ignore-warnings dont exit on warnings, be careful when using with --script" 38 | echo " --hook-pre-sync script to be run prior to main sync process" 39 | echo " --hook-post-sync script to be run after main sync process, before unmount" 40 | echo " --rsync-verbose list files as they are rsynced" 41 | echo " --rsync-dry-run run rsync in dry-run mode" 42 | echo 43 | echo "Config:" 44 | while read -r CONF_FILE; do 45 | echo " ${CONF_FILE}" 46 | done <<< "$CONF_LIST" 47 | echo 48 | echo "Examples:" 49 | echo " sudo clonepi /dev/sdb --init-destination --rsync-verbose" 50 | echo " sudo clonepi 6891fb21-30be-4cd5-88bf-e973b3bc08b7 --script --services=apache2,mysql" 51 | echo " sudo clonepi /mnt/nas/system/backups/pi-plex.img --script --quiet" 52 | echo 53 | } 54 | doMsg() 55 | { 56 | # msg, type 57 | case "$2" in 58 | msg) 59 | # these are important messages, cannot be turned off 60 | printf "${1}\n" 61 | ;; 62 | info) 63 | # info can be turned off 64 | if [ $QUIET_MODE = false ]; then 65 | printf "Info: ${1}\n" 66 | fi 67 | ;; 68 | warn|warn-no-abort) 69 | # warnings normally cause abort, warn-no-abort doesnt 70 | printf "Warning: ${1}\n" 71 | WITH_WARNINGS=true 72 | if [ $EXIT_ON_WARNING = true -a "$2" = "warn" ]; then 73 | doMsg "not continuing, check warning and re-run ClonePi. Use --ignore-warnings to suppress this error." "error" 74 | fi 75 | ;; 76 | user-abort) 77 | cleanupLoopDevice 78 | printf "User Abort: ${1}\n" 79 | echo 80 | exit 0 81 | ;; 82 | error|error-report) 83 | # errors cause abort, error-reoprt doesnt 84 | printf "Error: ${1}\n" 85 | if [ "$2" = "error" ]; then 86 | cleanupLoopDevice 87 | echo "Aborting!" 88 | echo 89 | exit 1 90 | fi 91 | ;; 92 | *) 93 | printf "UNKNOWN MESSAGE TYPE: ${2}\n" 94 | doMsg $1 "error" 95 | ;; 96 | esac 97 | } 98 | refreshDevice() 99 | { 100 | # 101 | # refresh the $1 device 102 | # called at various points to refresh the partition table before running mount/disk probe commands 103 | # 104 | # this was a very vexxing problem - `mount` keeps reporting the cloned disk's /dev/sdb2 partition as mounted to the root dir 105 | # this obviously isnt the case! mount is getting confused becasue the clone partition has the same PARTUUID 106 | # refreshing the partition table twice(!) seemed to resolve 107 | # TODO: may need to consider changing the clone's PARTUUID for duration of clonepi process and changing back at end - just so we can rely on `mount` output 108 | # 109 | partprobe $1 110 | partprobe $1 111 | } 112 | readUserUnmount() 113 | { 114 | # prompt user to unmount location 115 | # (unless running in --script mode) 116 | # will exit 0 if user cancels or exit 1 if unmount did not complete 117 | if [ $SCRIPT_MODE = true ]; then 118 | UI="y" 119 | else 120 | echo 121 | read -p "Unmount $1 (yes|no)? " UI < /dev/tty 122 | fi 123 | if [ "$UI" = "y" -o "$UI" = "yes" ]; then 124 | if ! umount $1; then 125 | doMsg "could not unmount $1" "error" 126 | fi 127 | else 128 | doMsg "could not unmount $1" "user-abort" 129 | fi 130 | } 131 | cleanupLoopDevice() 132 | { 133 | # 134 | # remove loop device if its been setup 135 | # 136 | if [ "$LOOP_DEVICE" != "" ]; then 137 | losetup -d $LOOP_DEVICE 138 | if [ $? = 0 ]; then 139 | doMsg "deleted loop device $LOOP_DEVICE created for the cloning process" "info" 140 | else 141 | doMsg "unable to delete the loop device $LOOP_DEVICE created for the cloning process - you should delete this manually or reboot" "warn-no-abort" 142 | fi 143 | fi 144 | } 145 | cleanupSourceMounts() 146 | { 147 | if [ "$UNMOUNT_SRC_DIRS" != "" ]; then 148 | # refresh source disk partition tables before using mount 149 | refreshDevice $SRC_DISK 150 | # remove the partition dirs 151 | while read -r SRC_DIR; do 152 | if umount $SRC_DIR; then 153 | doMSg "unmounted source partition from $SRC_DIR" "info" 154 | if rmdir $SRC_DIR; then 155 | doMsg "deleted empty source partition dir $SRC_DIR" "info" 156 | else 157 | doMsg "could not delete source partition dir $SRC_DIR, perhaps it's not empty?" "warn" 158 | fi 159 | else 160 | doMsg "could not unmount source partiton from ${SRC_DIR}, this shouldn't really happen." "warn" 161 | fi 162 | done <<< "$UNMOUNT_SRC_DIRS" 163 | # & remove the source mount dir 164 | if rmdir $SRC_MOUNT_DIR; then 165 | doMsg "removed source dir $SRC_MOUNT_DIR" "info" 166 | else 167 | doMsg "could not remove source dir $SRC_MOUNT_DIR, perhaps it's not empty?" "warn" 168 | fi 169 | fi 170 | } 171 | cleanupDestinationMounts() 172 | { 173 | # remove partition dirs 174 | while read -r DEST_DIR; do 175 | if umount $DEST_DIR; then 176 | doMsg "unmounted destination partition from $DEST_DIR" "info" 177 | if rmdir $DEST_DIR; then 178 | doMsg "deleted empty destination partition dir $DEST_DIR" "info" 179 | else 180 | doMsg "could not delete destination partition dir $DEST_DIR, perhaps it's not empty?" "warn" 181 | fi 182 | else 183 | doMsg "could not unmount destination partition from ${DEST_DIR}, this shouldn't really happen." "warn" 184 | fi 185 | done <<< "$DEST_SYNC_DIRS" 186 | # & remove the destination mount dir 187 | if rmdir $CLONE_MOUNT_DIR; then 188 | doMsg "removed clone dir $CLONE_MOUNT_DIR" "info" 189 | else 190 | doMsg "could not remove clone dir $CLONE_MOUNT_DIR, perhaps it's not empty?" "warn" 191 | fi 192 | } 193 | doCleanup() 194 | { 195 | # 196 | # cleanup requires setup has been run 197 | # 198 | if [ $WAIT_BEFORE_UNMOUNT = true -a $SCRIPT_MODE = false ]; then 199 | echo 200 | read -p "Press ENTER to continue with cleanup... " UI < /dev/tty 201 | else 202 | doMsg "cleaning up..." "info" 203 | fi 204 | 205 | if [ $INIT_DEST_FILE = true -a "$CLONE_TO" = "file" ]; then 206 | # full dd clone was done 207 | if [ $TRIM_SOURCE = true ]; then 208 | # cleanup the source mounts 209 | cleanupSourceMounts 210 | fi 211 | else 212 | # standard device clone (or clone to file via loop) - cleanup 213 | # remove loop device if created for clone process 214 | cleanupLoopDevice 215 | # unmount and remove the source dirs - the ones we mounted to do the sync 216 | cleanupSourceMounts 217 | # umount and remove all destination dirs 218 | cleanupDestinationMounts 219 | fi 220 | # refresh all filesystems 221 | sync 222 | } 223 | 224 | 225 | 226 | ################################################# 227 | # setup 228 | ################################################# 229 | 230 | echo 231 | # 232 | # must be run as root 233 | # 234 | if [ `id -u` != 0 ]; then 235 | doMsg "ClonePi needs to be run as root" "error" 236 | fi 237 | 238 | # 239 | # config 240 | # 241 | CONF_DIR="/etc/clonepi" 242 | 243 | if [ -f ${CONF_DIR}/clonepi.conf ]; then 244 | . ${CONF_DIR}/clonepi.conf 245 | CONF_LIST=`ls -d -1 ${CONF_DIR}/**` 246 | else 247 | doMsg "${CONF_DIR}/clonepi.conf not found! Please run the installer." "error" 248 | fi 249 | 250 | # 251 | # state 252 | # TODO: complete this and comment each 253 | # 254 | # general 255 | START_TIME="" 256 | FORCE_DEST_DISK_INIT=false 257 | RESIZE_DEST_DISK=false 258 | SRC_LARGER_THAN_DEST=false 259 | CROSS_FILESYSTEMS=false 260 | SCRIPT_MODE=false 261 | WITH_WARNINGS=false 262 | EXIT_ON_WARNING=true 263 | WAIT_BEFORE_UNMOUNT=false 264 | COMPRESS_FILE=false 265 | QUIET_MODE=false 266 | SERVICE_LIST="" 267 | SERVICES_STOPPED=false 268 | HOOK_PRE_SYNC="" 269 | HOOK_PRE_SYNC_DONE=false 270 | HOOK_POST_SYNC="" 271 | CLONE_TO="" # one of: dev, uuid, file 272 | TRIM_SOURCE=false 273 | # source disk 274 | SRC_DISK_PART_PREFIX="p" 275 | SRC_SECTORS="" 276 | SRC_PARTS="" 277 | SRC_DISK_NAME="${SRC_DISK#/dev/}" 278 | SRC_EXTENDED_PART_NUM=0 279 | # destination disk 280 | DEST_DISK_PART_PREFIX="" 281 | DEST_SECTORS="" 282 | DEST_DISK="" 283 | DEST_DISK_NAME="" 284 | DEST_DISK_READY=false 285 | DEST_PARTS="" 286 | LOOP_DEVICE="" 287 | INIT_DEST_FILE=false 288 | 289 | # 290 | # parse args 291 | # 292 | while [ "$1" ]; do 293 | case "$1" in 294 | -h|--help) 295 | usageInfo 296 | exit 0 297 | ;; 298 | -v|--version) 299 | usageInfo "version-only" 300 | exit 0 301 | ;; 302 | --quiet|q) 303 | QUIET_MODE=true 304 | ;; 305 | --init-destination) 306 | FORCE_DEST_DISK_INIT=true 307 | ;; 308 | --fill-destination) 309 | RESIZE_DEST_DISK=true 310 | FORCE_DEST_DISK_INIT=true 311 | ;; 312 | --rsync-verbose) 313 | RSYNC_OPTIONS="${RSYNC_OPTIONS}v" 314 | ;; 315 | --rsync-dry-run) 316 | RSYNC_OPTIONS="${RSYNC_OPTIONS}n" 317 | ;; 318 | --script) 319 | SCRIPT_MODE=true 320 | ;; 321 | --ignore-warnings) 322 | EXIT_ON_WARNING=false 323 | ;; 324 | --wait-before-unmount) 325 | WAIT_BEFORE_UNMOUNT=true 326 | ;; 327 | --services|--services=*) 328 | if [[ $1 = *"="* ]]; then 329 | # = used between switch and path 330 | SERVICE_LIST=`echo "$1" | cut -f2 -d'='` 331 | else 332 | # space used between switch and path 333 | shift 334 | SERVICE_LIST=$1 335 | fi 336 | ;; 337 | --hook-pre-sync|--hook-pre-sync=*) 338 | if [[ $1 = *"="* ]]; then 339 | # = used between switch and path 340 | HOOK_PRE_SYNC=`echo "$1" | cut -f2 -d'='` 341 | else 342 | # space used between switch and path 343 | shift 344 | HOOK_PRE_SYNC=$1 345 | fi 346 | ;; 347 | --hook-post-sync|--hook-post-sync=*) 348 | if [[ $1 = *"="* ]]; then 349 | # = used between switch and path 350 | HOOK_POST_SYNC=`echo "$1" | cut -f2 -d'='` 351 | else 352 | # space used between switch and path 353 | shift 354 | HOOK_POST_SYNC=$1 355 | fi 356 | ;; 357 | --compress-file) 358 | COMPRESS_FILE=true 359 | ;; 360 | --trim-source) 361 | TRIM_SOURCE=true 362 | ;; 363 | *) 364 | if [ "$DEST_DISK" != "" ]; then 365 | usageInfo 366 | exit 1 367 | fi 368 | DEST_DISK=$1 369 | ;; 370 | esac 371 | shift 372 | done 373 | if [ "$DEST_DISK" = "" ]; then 374 | usageInfo 375 | exit 1 376 | fi 377 | 378 | # 379 | # initial user feedback + finish pre-clone setup 380 | # 381 | # message if running in --script mode 382 | if [ $SCRIPT_MODE = true ]; then 383 | INIT_TIME=`date '+%Y-%m-%dT%H:%M:%S'` 384 | doMsg "ClonePi started in non-interactive mode : $INIT_TIME" "msg" 385 | if [ $EXIT_ON_WARNING = true ]; then 386 | doMsg "all user input assumed yes, until a warning is hit." "info" 387 | else 388 | doMsg "running with --ignore-warnings. All user input assumed yes, regardless of warnings." "warn" 389 | fi 390 | # when running without a user, confirm the destination (useful for looking at logs later) 391 | doMsg "Cloning to : $DEST_DISK" "msg" 392 | fi 393 | # check if PATH has sbin - if not use the EXPORT_PATH from conf 394 | # because cron generally has a limited environment setup - this makes invoking ClonePi from cron easier 395 | CHECKPATH=`echo $PATH | grep sbin` 396 | if [ -z "$CHECKPATH" ]; then 397 | if [ -n "$EXPORT_PATH" ]; then 398 | doMsg "sbin not found in PATH, exporting PATH=$EXPORT_PATH" "info" 399 | export PATH=$EXPORT_PATH 400 | fi 401 | fi 402 | # check if udisks2 service is running, known to cause issues: https://github.com/SpoddyCoder/clonepi/issues/2 403 | if [ -x "$(command -v systemctl)" ]; then 404 | servicecheck=`systemctl list-unit-files | grep enabled | grep udisks2 | wc -l` 405 | if [ $servicecheck = 1 ]; then 406 | servicerunning=`systemctl is-active --quiet udisks2` 407 | if [ $? -eq 0 ]; then 408 | # udisks running, show warning if not specified in --services list 409 | if ! [[ $SERVICE_LIST == *"udisks2"* ]]; then 410 | doMsg "udisks2 service is running, consider using the --services switch to stop/start it, check README for more info" "warn" 411 | fi 412 | fi 413 | fi 414 | fi 415 | 416 | ################################################# 417 | # determine & setup destination 418 | ################################################# 419 | 420 | DEST_DISK_DIR=`expr substr $DEST_DISK 1 5` 421 | if [ $DEST_DISK_DIR == "/dev/" ]; then 422 | # 423 | # looks like a standard device identifier 424 | # 425 | CLONE_TO="dev" 426 | if [ $COMPRESS_FILE = true ]; then 427 | doMsg "--compress-file does not apply when cloning to a device" "error" 428 | fi 429 | if [ $SCRIPT_MODE = true ]; then 430 | doMsg "it is recommended to use the device UUID when running in non-interactive mode." "warn" 431 | fi 432 | else 433 | # check if this is a file path (UUIDs dont start with a /) 434 | DEST_DISK_DIR_ROOT=`expr substr $DEST_DISK 1 1` 435 | if [ ! $DEST_DISK_DIR_ROOT == "/" ]; then 436 | # 437 | # looks like a UUID - look up the source disk 438 | # 439 | CLONE_TO="uuid" 440 | if [ $COMPRESS_FILE = true ]; then 441 | doMsg "--compress-file does not apply when cloning to a device" "error" 442 | fi 443 | SRCPARTLOOKUP=`findfs UUID=${DEST_DISK}` 444 | if [ "$SRCPARTLOOKUP" == "" ];then 445 | doMsg "check that the disk is plugged in and you have used the correct UUID" "error" 446 | else 447 | # findfs will return a partition - get parent block device name 448 | DEST_DISK="/dev/"`lsblk -no pkname $SRCPARTLOOKUP` 449 | doMsg "UUID used, destination disk identified as : ${DEST_DISK}" "info" 450 | fi 451 | else 452 | # 453 | # looks like a file path - perform checks & init loop device 454 | # 455 | CLONE_TO="file" 456 | if [ ! -f $DEST_DISK ]; then 457 | # file doesnt exist 458 | if [ $FORCE_DEST_DISK_INIT = false ]; then 459 | doMsg "destination file $DEST_DISK does not exist and will need to be created.\nRe-run with --init-destination to remove this warning." "warn" 460 | fi 461 | if [ $COMPRESS_FILE = true ]; then 462 | doMsg "--compress-file will not allow this clone to be incrementally updated" "info" 463 | fi 464 | INIT_DEST_FILE=true 465 | else 466 | # file exists 467 | if [ $COMPRESS_FILE = true ]; then 468 | # compressing file will force init 469 | doMsg "destination file $DEST_DISK exists and will be overwritten" "info" 470 | doMsg "--compress-file will not allow this clone to be incrementally updated" "info" 471 | INIT_DEST_FILE=true 472 | else 473 | if [ $FORCE_DEST_DISK_INIT = true ]; then 474 | doMsg "destination file $DEST_DISK exists and will be overwritten" "info" 475 | INIT_DEST_FILE=true 476 | else 477 | # finally, check if it's a previous disk image 478 | DEST_DISK_CHECK=`fdisk -l $DEST_DISK` 479 | if [ "$?" = 0 ]; then 480 | # check the image size 481 | SRC_SECTORS=`fdisk -l $SRC_DISK | grep "Disk $SRC_DISK" | cut -f 3 -d, | xargs | sed s/sectors//` 482 | DEST_SECTORS=`fdisk -l $DEST_DISK | grep "Disk $DEST_DISK" | cut -f 3 -d, | xargs | sed s/sectors//` 483 | if [ $SRC_SECTORS != $DEST_SECTORS ]; then 484 | # sectors dont match 485 | if [ $FORCE_DEST_DISK_INIT = true ]; then 486 | doMsg "destination file $DEST_DISK exists, but the size does not match the source disk $SRC_DISK.\nA new clone will be created" "info" 487 | else 488 | doMsg "destination file $DEST_DISK exists, but the size does not match the source disk $SRC_DISK.\nRe-run with --init-destination to remove this warning." "warn" 489 | fi 490 | INIT_DEST_FILE=true 491 | else 492 | # file exists & matches source 493 | doMsg "destination file $DEST_DISK exists and will be updated" "info" 494 | fi 495 | else 496 | # not an image 497 | if [ $FORCE_DEST_DISK_INIT = true ]; then 498 | doMsg "destination file $DEST_DISK exists, but is not an image file and will be overwritten.\n" "info" 499 | else 500 | doMsg "destination file $DEST_DISK exists, but is not an image file.\nRe-run with --init-destination to remove this warning." "warn" 501 | fi 502 | INIT_DEST_FILE=true 503 | fi 504 | fi 505 | fi 506 | fi 507 | 508 | if [ $INIT_DEST_FILE = false ]; then 509 | # 510 | # setup loop device to the destination file - which will enable us to use the same clone process :) 511 | # 512 | CHECK_LOOP=`losetup -j $DEST_DISK --list --noheadings --output NAME` 513 | if [ "$CHECK_LOOP" != "" ]; then 514 | # this file already has a loop device setup, warn the user 515 | doMsg "a loop device for the destination file already exists: $CHECK_LOOP.\nIf you choose to continue a new loop device will be created. To delete it, run: sudo losetup -d $CHECK_LOOP" "warn" 516 | fi 517 | LOOP_DEVICE=`losetup --partscan --find --show $DEST_DISK` 518 | if [ $? = 0 ]; then 519 | # loop device setup ok, set it as the destination 520 | doMsg "new loop device created for the clone : $LOOP_DEVICE" "info" 521 | DEST_DISK=$LOOP_DEVICE 522 | # occasionally see I/O warning when probing the new loop device (not yet solved) 523 | # Warning: Error fsyncing/closing /dev/loop0p1: Input/output error 524 | # ... parted still succeeds tho :/ 525 | # so, lets force the warning now & throw it away 526 | DEST_PART_CHECK=`parted -s $DEST_DISK p >/dev/null 2>&1` 527 | else 528 | doMsg "unable to setup the loop device for $DEST_DISK" "error" 529 | fi 530 | fi 531 | fi 532 | fi 533 | # get the device name without the path 534 | DEST_DISK_NAME=${DEST_DISK#/dev/} 535 | 536 | 537 | ################################################# 538 | # finish pre-flight checks 539 | ################################################# 540 | 541 | if [ $INIT_DEST_FILE = true -a "$CLONE_TO" = "file" ]; then 542 | # if cloning to a file & initialising, then this is done by full dd clone process 543 | # no need for device checks 544 | : 545 | 546 | else 547 | # cloning to a device (or a file with a loop device setup) 548 | # preform device checks 549 | 550 | # check destination != source 551 | if [ "$DEST_DISK" = "$SRC_DISK" ]; then 552 | doMsg "destination disk $DEST_DISK is same as the source disk." "error" 553 | fi 554 | 555 | # check the DEST_DISK_NAME exists on the system 556 | # NB \b for exact match 557 | if ! cat /proc/partitions | grep -q "\b$DEST_DISK_NAME\b"; then 558 | doMsg "destination disk $DEST_DISK does not exist.\nCheck the disk is plugged in and and you have used the correct disk name." "error" 559 | fi 560 | # check if DEST_DISK_NAME ends with a number 561 | # if yes - check if it looks like a partition and give a warning 562 | # if no - apply partition prefix 563 | if [[ ${DEST_DISK_NAME: -1} =~ ^-?[0-9]+$ ]]; then 564 | # lsblk will respond with >1 lines for parent block device, 1 line for a partition 565 | # NB: this will not pick up partitions on loop devices - not found a way to disambiguate yet! 566 | PARTCHECK=`lsblk -n -o type $DEST_DISK` 567 | if [ "$PARTCHECK" == "part" ]; then 568 | doMsg "$DEST_DISK ends with a number and looks like a partition rather than a disk.\nOnly continue if you are sure this is the disk you want." "warn" 569 | fi 570 | # apply partition prefix to destination 571 | DEST_DISK_PART_PREFIX="p" 572 | fi 573 | 574 | # check that the CLONE_MOUNT_DIR and SRC_MOUNT_DIR are not in use 575 | IS_MOUNTED=`fgrep "\b$CLONE_MOUNT_DIR\b" /etc/mtab | awk '{print $1}'` 576 | if [ "$IS_MOUNTED" != "" ]; then 577 | doMsg "${CLONE_MOUNT_DIR}/ is mounted with: $IS_MOUNTED" "warn-no-abort" 578 | readUserUnmount $CLONE_MOUNT_DIR 579 | fi 580 | if [ -d $CLONE_MOUNT_DIR ]; then 581 | IS_EMPTY=`ls -A $CLONE_MOUNT_DIR` 582 | if [ "$IS_EMPTY" != "" ]; then 583 | doMsg "${CLONE_MOUNT_DIR}/ is not empty - contents could be erased if you choose to continue." "warn" 584 | fi 585 | fi 586 | IS_MOUNTED=`fgrep "\b$SRC_MOUNT_DIR\b" /etc/mtab | awk '{print $1}'` 587 | if [ "$IS_MOUNTED" != "" ]; then 588 | doMsg "${SRC_MOUNT_DIR}/ is mounted with: $IS_MOUNTED" "warn-no-abort" 589 | readUserUnmount $SRC_MOUNT_DIR 590 | fi 591 | if [ -d $SRC_MOUNT_DIR ]; then 592 | IS_EMPTY=`ls -A $SRC_MOUNT_DIR` 593 | if [ "$IS_EMPTY" != "" ]; then 594 | doMsg "${SRC_MOUNT_DIR}/ is not empty." "warn" 595 | fi 596 | fi 597 | 598 | # check that none of the destination partitions are currently mounted 599 | while read -r DEST_PART; do 600 | DEST_PART_NUM=`echo $DEST_PART | awk '{print $1}'` 601 | DEST_PART_DEV="${DEST_DISK}${DEST_PART_NUM}" 602 | DEST_PART_MTAB=`cat /etc/mtab | fgrep $DEST_PART_DEV` 603 | if [ ! "$DEST_PART_MTAB" == "" ]; then 604 | DEST_PART_MOUNT=`echo $DEST_PART_MTAB | awk '{print $2}'` 605 | doMsg "destination partition $DEST_PART_DEV is currently mounted to $DEST_PART_MOUNT" "warn-no-abort" 606 | readUserUnmount $DEST_PART_MOUNT 607 | fi 608 | done <<< "$DEST_PARTS" 609 | 610 | # check the DEST_DISK structure matches SRC_DISK structure 611 | # NB: not using machine readable flag here as we need to spot extended partitions - which doesn't appear to be available using -m 612 | SRC_PARTS=`parted -s $SRC_DISK p | grep "^ "` 613 | DEST_PARTS=`parted -s $DEST_DISK p | grep "^ "` 614 | CNT=1 615 | PARTS_OK=true 616 | while read -r SRC_PART; do 617 | # number, type, filesystem & flags should be identical 618 | SRC_PART_NUM=`echo $SRC_PART | awk '{print $1}'` 619 | SRC_PART_TYPE=`echo $SRC_PART | awk '{print $5}'` 620 | SRC_PART_FS=`echo $SRC_PART | awk '{print $6}'` 621 | SRC_PART_FLAGS=`echo $SRC_PART | awk '{print $7}'` 622 | DEST_PART=`sed -n ${CNT}p <<< "$DEST_PARTS"` # if dest part CNT not available then will fail the match, so no issue 623 | DEST_PART_NUM=`echo $DEST_PART | awk '{print $1}'` 624 | DEST_PART_TYPE=`echo $DEST_PART | awk '{print $5}'` 625 | DEST_PART_FS=`echo $DEST_PART | awk '{print $6}'` 626 | DEST_PART_FLAGS=`echo $DEST_PART | awk '{print $7}'` 627 | if [ ! "$SRC_PART_NUM" == "$DEST_PART_NUM" -o ! "$SRC_PART_TYPE" == "$DEST_PART_TYPE" -o ! "$SRC_PART_FS" == "$DEST_PART_FS" -o ! "$SRC_PART_FLAGS" == "$DEST_PART_FLAGS" ]; then 628 | PARTS_OK=false 629 | fi 630 | CNT=$((CNT+1)) 631 | done <<< "$SRC_PARTS" 632 | if $PARTS_OK; then 633 | DEST_DISK_READY=true 634 | else 635 | DEST_DISK_READY=false 636 | fi 637 | if [ $DEST_DISK_READY = false -a $FORCE_DEST_DISK_INIT = false ]; then 638 | if [ $FORCE_DEST_DISK_INIT = true ]; then 639 | doMsg "the source & destination disk partition structures do not match.\nThe destination disk will be formatted to match the source, all existing data on it will be lost." "info" 640 | else 641 | doMsg "the source & destination disk partition structures do not match.\nRe-run with --init-destination to remove this warning." "warn" 642 | fi 643 | fi 644 | 645 | # check src/dest disk sizes - set flags & show info/warnings 646 | SRC_SECTORS=`fdisk -l $SRC_DISK | grep "Disk $SRC_DISK" | cut -f 3 -d, | xargs | sed s/sectors//` 647 | DEST_SECTORS=`fdisk -l $DEST_DISK | grep "Disk $DEST_DISK" | cut -f 3 -d, | xargs | sed s/sectors//` 648 | if [ $SRC_SECTORS != $DEST_SECTORS ]; then 649 | if (( SRC_SECTORS > DEST_SECTORS )); then 650 | # src > dest - set flag 651 | SRC_LARGER_THAN_DEST=true 652 | if [ $FORCE_DEST_DISK_INIT = true ]; then 653 | # init-ing disk, so add resize flag and show info + warning 654 | doMsg "destination disk $DEST_DISK is smaller than the source disk ${SRC_DISK}\nClonePi will attempt to resize the last partition on the destination,\nbut this may result in a partition too small to take all of the content." "warn" 655 | RESIZE_DEST_DISK=true 656 | else 657 | # not init-ing disk, so only show info 658 | doMsg "destination disk $DEST_DISK is smaller than the source disk ${SRC_DISK}\nThe source content may not fit on the destination." "warn" 659 | fi 660 | else 661 | # src < dest 662 | SHOW_INFO=false 663 | if [ $FORCE_DEST_DISK_INIT = true -a $RESIZE_DEST_DISK = false ]; then 664 | # init-ind disk but not resizing, show info that they can resize 665 | SHOW_INFO=true 666 | fi 667 | if [ $FORCE_DEST_DISK_INIT = false -a $RESIZE_DEST_DISK = false ]; then 668 | # not init-ing disk, disk must be ready (force init flag would be set if not) - show info if last partition is not at max size for disk (within 5 sectors) 669 | LAST_PART_END=`fdisk -lu $DEST_DISK | grep "^$DEST_DISK" | tail -1 | awk '{print $3}'` 670 | if [ "$LAST_PART_END" != "" ]; then 671 | SIZE_DIFF=$((DEST_SECTORS - LAST_PART_END)) 672 | SIZE_DIFF=${SIZE_DIFF/#-} # abs 673 | if (( SIZE_DIFF > 5 )); then 674 | SHOW_INFO=true 675 | fi 676 | fi 677 | fi 678 | if [ $SHOW_INFO = true ]; then 679 | doMsg "destination disk $DEST_DISK is larger than the source disk ${SRC_DISK}\nYou can use --fill-destination to resize the last partition to fill the entire disk." "info" 680 | fi 681 | fi 682 | fi 683 | fi 684 | 685 | # check services exist if specified 686 | if [ "$SERVICE_LIST" != "" ]; then 687 | for SERVICE in ${SERVICE_LIST//,/ }; do 688 | servicecheck=`systemctl list-unit-files | grep enabled | grep "${SERVICE}.service" | wc -l` 689 | if [ $servicecheck != 1 ]; then 690 | doMsg "could not find ${SERVICE}.service" "warn" 691 | fi 692 | done 693 | fi 694 | 695 | # check pre & post sync hooks exist if specified 696 | if [ "$HOOK_PRE_SYNC" != "" ]; then 697 | if [ ! -f $HOOK_PRE_SYNC ]; then 698 | doMsg "the pre-sync script : $HOOK_PRE_SYNC does not exist" "warn" 699 | fi 700 | fi 701 | if [ "$HOOK_POST_SYNC" != "" ]; then 702 | if [ ! -f $HOOK_POST_SYNC ]; then 703 | doMsg "the post-sync script : $HOOK_POST_SYNC does not exist" "warn" 704 | fi 705 | fi 706 | 707 | # 708 | # final setup 709 | # 710 | 711 | # get lists of source and destination sync dirs & devices - for use in the main process 712 | # we'll mount any src partitions not yet mounted, and use the current mount point for those that are mounted already 713 | # TODO: this is very 'bashy' - change to arrays will probably improve code maintainability 714 | 715 | # .. get currently mounted src partitions & devices 716 | DEST_DEVS="" 717 | DEST_SYNC_DIRS="" 718 | # refresh device before probing 719 | refreshDevice $SRC_DISK 720 | SRC_DEVS=`mount | grep $SRC_DISK | awk '{print $1}'` 721 | SRC_SYNC_DIRS=`mount | grep $SRC_DISK | awk '{print $3}'` 722 | # .. and generate their corresponding dest mount points & devices 723 | while read -r SRC_DEV; do 724 | SRC_PART_NUM="${SRC_DEV/${SRC_DISK}}" 725 | SRC_PART_NUM="${SRC_PART_NUM/${SRC_DISK_PART_PREFIX}}" # get just the ordinal, without any prefix 726 | DEST_SYNC_DIR="${CLONE_MOUNT_DIR}/${DEST_DISK_NAME}${DEST_DISK_PART_PREFIX}${SRC_PART_NUM}" 727 | DEST_SYNC_DIRS="${DEST_SYNC_DIRS}${DEST_SYNC_DIR}"$'\n' 728 | DEST_DEV="${DEST_DISK}${DEST_DISK_PART_PREFIX}${SRC_PART_NUM}" 729 | DEST_DEVS="${DEST_DEVS}${DEST_DEV}"$'\n' 730 | done <<< "$SRC_DEVS" 731 | SRC_DEVS="$SRC_DEVS"$'\n' # add trailing newline, ready for additional lines to be added below 732 | SRC_SYNC_DIRS="$SRC_SYNC_DIRS"$'\n' 733 | # .. now add the ones not mounted 734 | CNT=1 735 | while read -r SRC_PART; do 736 | SRC_PART_NUM=`echo $SRC_PART | awk '{print $1}'` 737 | SRC_PART_TYPE=`echo $SRC_PART | awk '{print $5}'` 738 | DEST_PART_NUM=$SRC_PART_NUM 739 | DEST_SYNC_DIR="${CLONE_MOUNT_DIR}/${DEST_DISK_NAME}${DEST_DISK_PART_PREFIX}${DEST_PART_NUM}" 740 | SRC_SYNC_DIR="${SRC_MOUNT_DIR}/${SRC_DISK_NAME}${SRC_DISK_PART_PREFIX}${SRC_PART_NUM}" 741 | SRC_DEV="${SRC_DISK}${SRC_DISK_PART_PREFIX}${SRC_PART_NUM}" 742 | DEST_DEV="${DEST_DISK}${DEST_DISK_PART_PREFIX}${DEST_PART_NUM}" 743 | if [ "$SRC_PART_TYPE" = "extended" ]; then 744 | SRC_EXTENDED_PART_NUM=$SRC_PART_NUM # we'll use this to identify extended partition later in clone process 745 | fi 746 | # TODO: might be better for belt and braces to do another mount lookup here instead of assuming DEST_SYNC_DIRS is correct 747 | ALREADY_MOUNTED=`echo $DEST_SYNC_DIRS | grep $DEST_SYNC_DIR` 748 | # don't include if mounted or an extended partition 749 | if [ "$ALREADY_MOUNTED" = "" -a "$SRC_PART_TYPE" != "extended" ]; then 750 | SRC_SYNC_DIRS="${SRC_SYNC_DIRS}${SRC_SYNC_DIR}"$'\n' 751 | SRC_DEVS="${SRC_DEVS}${SRC_DEV}"$'\n' 752 | DEST_SYNC_DIRS="${DEST_SYNC_DIRS}${DEST_SYNC_DIR}"$'\n' 753 | DEST_DEVS="${DEST_DEVS}${DEST_DEV}"$'\n' 754 | fi 755 | CNT=$((CNT+1)) 756 | done <<< "$SRC_PARTS" 757 | # .. cleanup our generated lists, remove trailing newline 758 | SRC_DEVS=`echo "$SRC_DEVS" | head -n -1` 759 | SRC_SYNC_DIRS=`echo "$SRC_SYNC_DIRS" | head -n -1` 760 | DEST_DEVS=`echo "$DEST_DEVS" | head -n -1` 761 | DEST_SYNC_DIRS=`echo "$DEST_SYNC_DIRS" | head -n -1` 762 | # get unmounted source partitions 763 | UNMOUNTED_SRC_PARTS="" 764 | CNT=1 765 | while read -r SRC_SYNC_DIR; do 766 | SRC_DEV=`sed -n ${CNT}p <<< "$SRC_DEVS"` 767 | ALREADY_MOUNTED=`mount | grep "$SRC_DEV"` 768 | if [ "$ALREADY_MOUNTED" = "" ]; then 769 | UNMOUNTED_SRC_PARTS="${UNMOUNTED_SRC_PARTS}${SRC_DEV}"$'\n' 770 | fi 771 | CNT=$((CNT+1)) 772 | done <<< "$SRC_SYNC_DIRS" 773 | if [ "$UNMOUNTED_SRC_PARTS" != "" ]; then 774 | UNMOUNTED_SRC_PARTS=`echo "$UNMOUNTED_SRC_PARTS" | head -n -1` 775 | fi 776 | 777 | 778 | ################################################# 779 | # summary + user confirmation 780 | ################################################# 781 | 782 | if [ $WITH_WARNINGS = true ]; then 783 | doMsg "Pre-flight checks completed with WARNINGS, proceed with caution" "msg" 784 | else 785 | doMsg "Pre-flight checks completed OK" "msg" 786 | fi 787 | doMsg "the following actions will be taken..." "info" 788 | TASK=1 789 | 790 | if [ $INIT_DEST_FILE = true -a "$CLONE_TO" = "file" ]; then 791 | # this is a full dd clone, slightly different flow of events 792 | 793 | # if trim is applied, we'll need to mount the unounted source partitions to trim them 794 | if [ $TRIM_SOURCE = true ]; then 795 | if [ ! "$UNMOUNTED_SRC_PARTS" = "" ]; then 796 | doMsg "${TASK}) currently unmounted source disk partitions will be mounted inside ${SRC_MOUNT_DIR}/:" "info" 797 | while read -r UNMOUNTED_SRC_PART; do 798 | doMsg " $UNMOUNTED_SRC_PART" "info" 799 | done <<< "$UNMOUNTED_SRC_PARTS" 800 | TASK=$((TASK+1)) 801 | fi 802 | doMsg "${TASK}) each mount point of the source disk will be trimmed (if supported):" "info" 803 | while read -r SRC_SYNC_DIR; do 804 | doMsg " $SRC_SYNC_DIR" "info" 805 | done <<< "$SRC_SYNC_DIRS" 806 | TASK=$((TASK+1)) 807 | fi 808 | 809 | if [ ! -z $HOOK_PRE_SYNC -a -f $HOOK_PRE_SYNC ]; then 810 | doMsg "${TASK}) pre-sync script at $HOOK_PRE_SYNC will be run" "info" 811 | TASK=$((TASK+1)) 812 | fi 813 | 814 | if [ "$SERVICE_LIST" != "" ]; then 815 | doMsg "${TASK}) ${SERVICE_LIST} service(s) will be stopped" "info" 816 | TASK=$((TASK+1)) 817 | fi 818 | 819 | if [ $COMPRESS_FILE = true ]; then 820 | doMsg "${TASK}) destinaton file ${DEST_DISK} will be created as an image of the source disk and compressed" "info" 821 | else 822 | doMsg "${TASK}) destinaton file ${DEST_DISK} will be created as an image of the source disk" "info" 823 | fi 824 | TASK=$((TASK+1)) 825 | 826 | else 827 | 828 | # standard clone to a device (or file via loop) 829 | 830 | if [ $FORCE_DEST_DISK_INIT = true -o $DEST_DISK_READY = false ]; then 831 | doMsg "${TASK}) destinaton disk ${DEST_DISK} will be initialised to match the source disk structure" "info" 832 | TASK=$((TASK+1)) 833 | fi 834 | 835 | if [ $RESIZE_DEST_DISK = true ]; then 836 | doMsg "${TASK}) the last partition on $DEST_DISK will be resized to fill the disk" "info" 837 | TASK=$((TASK+1)) 838 | fi 839 | 840 | if [ ! "$UNMOUNTED_SRC_PARTS" = "" ]; then 841 | doMsg "${TASK}) currently unmounted source disk partitions will be mounted inside ${SRC_MOUNT_DIR}/:" "info" 842 | while read -r UNMOUNTED_SRC_PART; do 843 | doMsg " $UNMOUNTED_SRC_PART" "info" 844 | done <<< "$UNMOUNTED_SRC_PARTS" 845 | TASK=$((TASK+1)) 846 | fi 847 | if [ $TRIM_SOURCE = true ]; then 848 | doMsg "${TASK}) each mount point of the source disk will be trimmed (if supported):" "info" 849 | while read -r SRC_SYNC_DIR; do 850 | doMsg " $SRC_SYNC_DIR" "info" 851 | done <<< "$SRC_SYNC_DIRS" 852 | TASK=$((TASK+1)) 853 | fi 854 | 855 | doMsg "${TASK}) destination disk partitions will be mounted inside ${CLONE_MOUNT_DIR}/:" "info" 856 | while read -r DEST_DEV; do 857 | doMsg " $DEST_DEV" "info" 858 | done <<< "$DEST_DEVS" 859 | TASK=$((TASK+1)) 860 | 861 | if [ ! -z $HOOK_PRE_SYNC -a -f $HOOK_PRE_SYNC ]; then 862 | doMsg "${TASK}) pre-sync script at $HOOK_PRE_SYNC will be run" "info" 863 | TASK=$((TASK+1)) 864 | fi 865 | 866 | if [ "$SERVICE_LIST" != "" ]; then 867 | doMsg "${TASK}) ${SERVICE_LIST} service(s) will be stopped" "info" 868 | TASK=$((TASK+1)) 869 | fi 870 | 871 | doMsg "${TASK}) filesystems will be synced:" "info" 872 | CNT=1 873 | while read -r SRC_SYNC_DIR; do 874 | DEST_SYNC_DIR=`sed -n ${CNT}p <<< "$DEST_SYNC_DIRS"` 875 | doMsg " $SRC_SYNC_DIR --> $DEST_SYNC_DIR" "info" 876 | CNT=$((CNT+1)) 877 | done <<< "$SRC_SYNC_DIRS" 878 | TASK=$((TASK+1)) 879 | 880 | fi 881 | 882 | 883 | # same for both dd clone & device clone flows 884 | 885 | if [ "$SERVICE_LIST" != "" ]; then 886 | doMsg "${TASK}) ${SERVICE_LIST} service(s) will be restarted" "info" 887 | TASK=$((TASK+1)) 888 | fi 889 | 890 | if [ ! -z $HOOK_POST_SYNC -a -f $HOOK_POST_SYNC ]; then 891 | doMsg "${TASK}) post-sync script at $HOOK_POST_SYNC will be run" "info" 892 | TASK=$((TASK+1)) 893 | fi 894 | 895 | if [ $WAIT_BEFORE_UNMOUNT = true ]; then 896 | doMsg "${TASK}) wait for user confirmation before continuing to..." "info" 897 | TASK=$((TASK+1)) 898 | fi 899 | 900 | doMsg "${TASK}) cleanup source + destination mounts & dirs" "info" 901 | TASK=$((TASK+1)) 902 | 903 | 904 | # get confirmation 905 | if [ $SCRIPT_MODE = true ]; then 906 | # confirmation implied in --script mode 907 | UI="y" 908 | else 909 | echo 910 | read -p "Do you wish to continue (yes|no)? " UI < /dev/tty 911 | fi 912 | if [ "$UI" = "y" -o "$UI" = "yes" ]; then 913 | START_TIME=`date '+%Y-%m-%dT%H:%M:%S'` 914 | START_TIMER=`date '+%s'` 915 | doMsg "User confirmed, starting clone process : $START_TIME" "msg" 916 | else 917 | doMsg "Clone process cancelled." "user-abort" 918 | fi 919 | 920 | 921 | ################################################# 922 | # clone process start 923 | ################################################# 924 | 925 | # force buffer cache to disk for all filesystems 926 | sync 927 | 928 | setupSourceMounts() 929 | { 930 | doMsg "setting up source directories and mounts..." "info" 931 | 932 | # create SRC_MOUNT_DIR & partition mount dirs 933 | if [ "$UNMOUNTED_SRC_PARTS" != "" ]; then 934 | if [ ! -d $SRC_MOUNT_DIR ]; then 935 | if mkdir $SRC_MOUNT_DIR; then 936 | doMsg "created source dir at $SRC_MOUNT_DIR" "info" 937 | else 938 | doMsg "could not create source dir at ${SRC_MOUNT_DIR}" "error" 939 | fi 940 | fi 941 | while read -r SRC_SYNC_DIR; do 942 | if [ ! -d $SRC_SYNC_DIR ]; then 943 | if mkdir $SRC_SYNC_DIR; then 944 | doMsg "created source partition dir at ${SRC_SYNC_DIR}" "info" 945 | else 946 | doMsg "could not create source partition dir at ${SRC_SYNC_DIR}" "error" 947 | fi 948 | fi 949 | done <<< "$SRC_SYNC_DIRS" 950 | fi 951 | 952 | # mount source partitions 953 | # refresh source disk partition tables before using mount 954 | refreshDevice $SRC_DISK 955 | CNT=1 956 | UNMOUNT_SRC_DIRS="" 957 | if [ "$SRC_SYNC_DIRS" != "" ]; then 958 | while read -r SRC_SYNC_DIR; do 959 | SRC_DEV=`sed -n ${CNT}p <<< "$SRC_DEVS"` 960 | ALREADY_MOUNTED=`mount | grep "$SRC_DEV"` 961 | if [ "$ALREADY_MOUNTED" = "" ]; then 962 | if mount $SRC_DEV $SRC_SYNC_DIR; then 963 | doMsg "mounted $SRC_DEV to $SRC_SYNC_DIR" "info" 964 | UNMOUNT_SRC_DIRS="${UNMOUNT_SRC_DIRS}${SRC_SYNC_DIR}"$'\n' 965 | else 966 | doMsg "could not mount ${SRC_DEV} to ${SRC_SYNC_DIR}" "error" 967 | fi 968 | fi 969 | CNT=$((CNT+1)) 970 | done <<< "$SRC_SYNC_DIRS" 971 | UNMOUNT_SRC_DIRS=`echo "$UNMOUNT_SRC_DIRS" | head -n -1` 972 | fi 973 | } 974 | setupDestinationMounts() 975 | { 976 | doMsg "setting up destination directories and mounts..." "info" 977 | 978 | # create CLONE_MOUNT_DIR & partition mount dirs 979 | if [ ! -d $CLONE_MOUNT_DIR ]; then 980 | if mkdir $CLONE_MOUNT_DIR; then 981 | doMsg "created clone dir at $CLONE_MOUNT_DIR" "info" 982 | else 983 | doMsg "could not create clone dir at ${CLONE_MOUNT_DIR}" "error" 984 | fi 985 | fi 986 | while read -r DEST_SYNC_DIR; do 987 | if [ ! -d $DEST_SYNC_DIR ]; then 988 | if mkdir $DEST_SYNC_DIR; then 989 | doMsg "created clone partition dir at ${DEST_SYNC_DIR}" "info" 990 | else 991 | doMsg "could not create clone partition dir at ${DEST_SYNC_DIR}" "error" 992 | fi 993 | fi 994 | done <<< "$DEST_SYNC_DIRS" 995 | 996 | # mount clone partitions 997 | CNT=1 998 | while read -r DEST_SYNC_DIR; do 999 | DEST_DEV=`sed -n ${CNT}p <<< "$DEST_DEVS"` 1000 | ALREADY_MOUNTED=`mount | grep "$DEST_DEV"` 1001 | if [ "$ALREADY_MOUNTED" = "" ]; then 1002 | if mount $DEST_DEV $DEST_SYNC_DIR; then 1003 | doMsg "mounted $DEST_DEV to $DEST_SYNC_DIR" "info" 1004 | else 1005 | doMsg "could not mount ${DEST_DEV} to ${DEST_SYNC_DIR}" "error" 1006 | fi 1007 | fi 1008 | CNT=$((CNT+1)) 1009 | done <<< "$DEST_SYNC_DIRS" 1010 | } 1011 | trimSource() 1012 | { 1013 | if [ $TRIM_SOURCE = true ]; then 1014 | doMsg "trimming source disk..." "info" 1015 | while read -r SRC_SYNC_DIR; do 1016 | FSTRIM=`fstrim -v $SRC_SYNC_DIR 2>&1` 1017 | if [ $? = 0 ]; then 1018 | doMsg "filesyste at $FSTRIM" "info" 1019 | else 1020 | doMsg "filesystem at $SRC_SYNC_DIR does not support TRIM" "info" 1021 | fi 1022 | done <<< "$SRC_SYNC_DIRS" 1023 | fi 1024 | } 1025 | 1026 | # start/stop all services in SERVICE_LIST if defined 1027 | stopServices() 1028 | { 1029 | if [ "$SERVICE_LIST" != "" -a $SERVICES_STOPPED = false ]; then 1030 | for SERVICE in ${SERVICE_LIST//,/ }; do 1031 | servicecheck=`systemctl list-unit-files | grep enabled | grep "${SERVICE}.service" | wc -l` 1032 | if [ $servicecheck = 1 ]; then 1033 | doMsg "stopping ${SERVICE}.service" "info" 1034 | servicestop=`systemctl stop ${SERVICE}.service` 1035 | fi 1036 | done 1037 | SERVICES_STOPPED=true 1038 | fi 1039 | } 1040 | startServices() 1041 | { 1042 | if [ "$SERVICE_LIST" != "" ]; then 1043 | for SERVICE in ${SERVICE_LIST//,/ }; do 1044 | servicecheck=`systemctl list-unit-files | grep enabled | grep "${SERVICE}.service" | wc -l` 1045 | if [ $servicecheck = 1 ]; then 1046 | doMsg "starting ${SERVICE}.service" "info" 1047 | servicestart=`systemctl start ${SERVICE}.service` 1048 | fi 1049 | done 1050 | fi 1051 | } 1052 | 1053 | 1054 | # invoke script hooks if defined 1055 | runPreSyncHook() 1056 | { 1057 | if [ ! -z $HOOK_PRE_SYNC -a -f $HOOK_PRE_SYNC ]; then 1058 | if [ $HOOK_PRE_SYNC_DONE = false ]; then 1059 | doMsg "running pre-sync script at $HOOK_PRE_SYNC" "info" 1060 | # run as subprocess, so it can access all clonepi variables and make controlled exit 1061 | (. $HOOK_PRE_SYNC) 1062 | EXIT_CODE=$? 1063 | HOOK_PRE_SYNC_DONE=true 1064 | if [ $EXIT_CODE != 0 ]; then 1065 | if [ $EXIT_CODE = 2 ]; then 1066 | doMsg "pre-sync script exited with error code: $EXIT_CODE" "info" 1067 | else 1068 | doMsg "pre-sync script exited with error code: $EXIT_CODE" "error-report" 1069 | doCleanup 1070 | doMsg "pre-sync script caused abort" "error" 1071 | fi 1072 | fi 1073 | fi 1074 | fi 1075 | } 1076 | runPostSyncHook() 1077 | { 1078 | if [ ! -z $HOOK_POST_SYNC -a -f $HOOK_POST_SYNC ]; then 1079 | doMsg "Running post-sync script at $HOOK_POST_SYNC" "info" 1080 | (. $HOOK_POST_SYNC) 1081 | EXIT_CODE=$? 1082 | if [ $EXIT_CODE != 0 ]; then 1083 | if [ $EXIT_CODE = 2 ]; then 1084 | doMsg "post-sync script exited with error code: $EXIT_CODE" "info" 1085 | else 1086 | doMsg "post-sync script exited with error code: $EXIT_CODE" "error-report" 1087 | doCleanup 1088 | doMsg "post-sync script caused abort" "error" 1089 | fi 1090 | fi 1091 | fi 1092 | } 1093 | 1094 | if [ $INIT_DEST_FILE = true -a "$CLONE_TO" = "file" ]; then 1095 | # 1096 | # full dd clone 1097 | # 1098 | if [ $TRIM_SOURCE = true ]; then 1099 | # if trimming we need to mount any unmounted source partitions 1100 | setupSourceMounts 1101 | trimSource 1102 | fi 1103 | 1104 | else 1105 | # 1106 | # standard device clone (or clone to file via loop) 1107 | # 1108 | 1109 | # 1110 | # run pre-sync script hook & stop services 1111 | # 1112 | runPreSyncHook 1113 | stopServices 1114 | 1115 | # 1116 | # format destination disk 1117 | # 1118 | if [ $FORCE_DEST_DISK_INIT = true -o $DEST_DISK_READY = false ]; then 1119 | # get start of src disk's final partition - floor integer in MB 1120 | LAST_PART_NUM=`parted $SRC_DISK -ms p | tail -1 | cut -f 1 -d:` 1121 | LAST_PART_START_M=$(parted $SRC_DISK -ms unit MB p | grep "^${LAST_PART_NUM}" | cut -f 2 -d: | sed s/MB// | tr "," "." | cut -f 1 -d.) 1122 | # add 1MB to make sure we dd some way into it 1123 | DD_COUNT=`expr $LAST_PART_START_M + 1` 1124 | # partial dd clone of src disk -> dest disk 1125 | doMsg "initialising $DEST_DISK, this may take some time..." "info" 1126 | doMsg "setting up destination partition structure, copying ${DD_COUNT}MB..." "info" 1127 | dd if=$SRC_DISK of=$DEST_DISK bs=1M count=$DD_COUNT 1128 | if [ ! "$?" = 0 ]; then 1129 | doMsg "$DEST_DISK partial image did not complete." "error" 1130 | fi 1131 | # cleanup partition - to avoid volume not properly unmounted warnings 1132 | LAST_PART_DEST_DEV="${DEST_DISK}${LAST_PART_NUM}" 1133 | doMsg "running filesystem repair on last partition ${LAST_PART_DEST_DEV}" "info" 1134 | fsck -p $LAST_PART_DEST_DEV &> /dev/null # discard errors 1135 | 1136 | if [ $RESIZE_DEST_DISK = true ]; then 1137 | if [ $SRC_LARGER_THAN_DEST = true ]; then 1138 | # resize down - parted wont run (Error: Can't have a partition outside the disk!) until the MBR is fixed up 1139 | # using this approach: http://gparted.org/h2-fix-msdos-pt.php 1140 | doMsg "Fixing MBR on ${DEST_DISK_NAME}..." "info" 1141 | # get current MBR 1142 | MBR=`sfdisk -d $DEST_DISK` 1143 | # change partition size(s) so they are within DEST_SECTORS 1144 | if [ ! "$SRC_EXTENDED_PART_NUM" = 0 ]; then 1145 | # modify extended partition 1146 | EXT_PART="${DEST_DISK}${SRC_EXTENDED_PART_NUM}" 1147 | EXT_PART_ESCAPED=$(echo "$EXT_PART" | sed 's/\//\\\//g') 1148 | EXT_PART_START=`fdisk -l -u $DEST_DISK | grep "^${EXT_PART}" | awk '{print $2}'` 1149 | EXT_PART_NEW_SIZE=$((DEST_SECTORS - EXT_PART_START)) 1150 | MBR_SEARCH_TERM=`echo "$MBR" | grep "^${EXT_PART}" | cut -f 2 -d, | xargs` 1151 | MBR_REPLACE_TERM="size= ${EXT_PART_NEW_SIZE}" 1152 | MBR=`echo "$MBR" | sed "/^${EXT_PART_ESCAPED}/s/${MBR_SEARCH_TERM}/${MBR_REPLACE_TERM}/"` 1153 | doMsg "...modifying extended partition ${EXT_PART}..." "info" 1154 | fi 1155 | # modify last partition 1156 | LAST_PART_ESCAPED=$(echo "$LAST_PART_DEST_DEV" | sed 's/\//\\\//g') 1157 | LAST_PART_START=`fdisk -l -u $DEST_DISK | grep "^${LAST_PART_DEST_DEV}" | awk '{print $2}'` 1158 | LAST_PART_NEW_SIZE=$((DEST_SECTORS - LAST_PART_START)) 1159 | MBR_SEARCH_TERM=`echo "$MBR" | grep "^${LAST_PART_DEST_DEV}" | cut -f 2 -d, | xargs` 1160 | MBR_REPLACE_TERM="size= ${LAST_PART_NEW_SIZE}" 1161 | MBR=`echo "$MBR" | sed "/^${LAST_PART_ESCAPED}/s/${MBR_SEARCH_TERM}/${MBR_REPLACE_TERM}/"` 1162 | doMsg "...modifying last partition ${LAST_PART_DEST_DEV}..." "info" 1163 | # write MBR back to disk 1164 | # NB: possible to get resource busy error from sfdisk, even tho it updates succesfully 1165 | # a brief pause the update seems to resolve - but it may be better to discard error and assume OK 1166 | sleep 1 1167 | sfdisk --force $DEST_DISK <<< "$MBR" 1168 | if [ "$?" = 0 ]; then 1169 | doMsg "... updated MBR OK" "info" 1170 | else 1171 | doMsg "there was a problem updating the MBR on $DEST_DISK" "error" 1172 | fi 1173 | # inform OS of partition table changes 1174 | refreshDevice $DEST_DISK 1175 | else 1176 | # resize up 1177 | LAST_PART_TYPE=`parted -ms $SRC_DISK p | tail -1 | cut -f 5 -d:` 1178 | doMsg "resizing last partition $LAST_PART_DEST_DEV to use remaining available space on ${DEST_DISK_NAME}..." "info" 1179 | if [ ! "$SRC_EXTENDED_PART_NUM" = 0 ]; then 1180 | # resize the extended partition 1181 | parted $DEST_DISK -ms unit s resizepart $SRC_EXTENDED_PART_NUM 100% 1182 | if [ "$?" = 0 ]; then 1183 | doMsg "...extended partition $SRC_EXTENDED_PART_NUM resized..." "info" 1184 | else 1185 | doMsg "resize of extended partition $SRC_EXTENDED_PART_NUM failed." "error" 1186 | fi 1187 | fi 1188 | # get start of dest disk's final partition - in sectors 1189 | LAST_PART_START_S=$(parted $DEST_DISK -ms unit s p | grep "^${LAST_PART_NUM}" | cut -f 2 -d:) 1190 | if [ "$SRC_EXTENDED_PART_NUM" = 0 ]; then 1191 | # delete and recreate the same part with new boundaries 1192 | parted $DEST_DISK -ms unit s rm $LAST_PART_NUM mkpart primary $LAST_PART_TYPE $LAST_PART_START_S 100% 1193 | else 1194 | # we're in an extended partition, create a logical partition 1195 | parted $DEST_DISK -ms unit s rm $LAST_PART_NUM mkpart logical $LAST_PART_TYPE $LAST_PART_START_S 100% 1196 | fi 1197 | if [ "$?" = 0 ]; then 1198 | # get start of dest disk's final partition - in sectors 1199 | doMsg "...resize of $LAST_PART_DEST_DEV completed OK" "info" 1200 | else 1201 | doMsg "resize of ${LAST_PART_DEST_DEV} failed." "error" 1202 | fi 1203 | fi 1204 | fi 1205 | 1206 | # rebuild last partition filesystem 1207 | LAST_PART_TYPE=`parted -ms $SRC_DISK p | tail -1 | cut -f 5 -d:` 1208 | doMsg "rebuilding $LAST_PART_TYPE filesystem on last partition ${LAST_PART_DEST_DEV}..." "info" 1209 | mkfs -t $LAST_PART_TYPE -F $LAST_PART_DEST_DEV 1210 | if [ "$?" = 0 ]; then 1211 | doMsg "...filesystem rebuild completed OK" "info" 1212 | else 1213 | doMsg "$LAST_PART_TYPE filesystem rebuild on ${LAST_PART_DEST_DEV} failed." "error" 1214 | fi 1215 | 1216 | # inform OS of partiton table changes 1217 | refreshDevice $DEST_DISK 1218 | 1219 | doMsg "$DEST_DISK initialised OK" "info" 1220 | fi 1221 | 1222 | # 1223 | # setup sync dirs 1224 | # 1225 | setupSourceMounts 1226 | trimSource 1227 | setupDestinationMounts 1228 | 1229 | fi 1230 | 1231 | # 1232 | # run pre-sync script hook & stop services 1233 | # 1234 | runPreSyncHook 1235 | stopServices 1236 | 1237 | # 1238 | # main clone process 1239 | # 1240 | if [ $INIT_DEST_FILE = true -a "$CLONE_TO" = "file" ]; then 1241 | # 1242 | # this is a full dd clone 1243 | # 1244 | # create clone file if doesn't exist 1245 | if ! touch $DEST_DISK ; then 1246 | doMsg "unable to create file $DEST_DISK" "error" 1247 | fi 1248 | # perform full dd copy 1249 | if [ $COMPRESS_FILE = true ]; then 1250 | # compress the output stream - will keep the Pi CPU busy! 1251 | doMsg "starting image file process & compressing output stream with gzip, this may take some time..." "info" 1252 | dd if=$SRC_DISK | gzip > $DEST_DISK 1253 | else 1254 | doMsg "starting image file process, this may take some time..." "info" 1255 | dd if=$SRC_DISK of=$DEST_DISK 1256 | fi 1257 | if [ $? = 0 ]; then 1258 | # all good - refresh partition table 1259 | refreshDevice $DEST_DISK 1260 | else 1261 | doMsg "there was a problem initialising the destination file $DEST_DISK" "error" 1262 | fi 1263 | 1264 | else 1265 | # 1266 | # rsync filesystems 1267 | # 1268 | doMsg "starting filesystem sync processes, this may take some time..." "info" 1269 | CNT=1 1270 | while read -r SRC_SYNC_DIR; do 1271 | DEST_SYNC_DIR=`sed -n ${CNT}p <<< "$DEST_SYNC_DIRS"` 1272 | doMsg "syncing: $SRC_SYNC_DIR --> $DEST_SYNC_DIR" "info" 1273 | 1274 | # apply correct excludes for the sync 1275 | EXCLUDES="" 1276 | EXCLUDES_FILE="" 1277 | 1278 | # current OS disk 1279 | if [ "$SRC_SYNC_DIR" = "/" ]; then 1280 | doMsg "this is the running root directory, applying OS excludes" "info" 1281 | EXCLUDES_FILE="--exclude-from=${OS_EXCLUDES_FILE}" 1282 | EXCLUDES="--exclude=${SRC_MOUNT_DIR} --exclude=${CLONE_MOUNT_DIR}" 1283 | # exclude dphys swapfile if using one 1284 | if [ -f /etc/dphys-swapfile ]; then 1285 | SWAPFILE=`cat /etc/dphys-swapfile | grep ^CONF_SWAPFILE | cut -f 2 -d=` 1286 | if [ "$SWAPFILE" != "" ]; then 1287 | EXCLUDES="${EXCLUDES} --exclude=$SWAPFILE" 1288 | fi 1289 | fi 1290 | fi 1291 | 1292 | # and sync 1293 | sync 1294 | if [ "$EXCLUDES" != "" ]; then 1295 | EXCLUDES=" ${EXCLUDES}" 1296 | fi 1297 | if [ "$EXCLUDES_FILE" != "" ]; then 1298 | EXCLUDES_FILE=" ${EXCLUDES_FILE}" 1299 | fi 1300 | SRC_SYNC_DIR=${SRC_SYNC_DIR%/} # remove trailing slash if exists (will be re-added below) 1301 | DEST_SYNC_DIR=${DEST_SYNC_DIR%/} 1302 | doMsg "rsync ${RSYNC_OPTIONS}${EXCLUDES}${EXCLUDES_FILE} ${SRC_SYNC_DIR}/ ${DEST_SYNC_DIR}/" "info" 1303 | rsync ${RSYNC_OPTIONS}${EXCLUDES}${EXCLUDES_FILE} ${SRC_SYNC_DIR}/ ${DEST_SYNC_DIR}/ 1304 | 1305 | CNT=$((CNT+1)) 1306 | done <<< "$SRC_SYNC_DIRS" 1307 | doMsg "...all sync processes finished OK" "info" 1308 | 1309 | fi 1310 | 1311 | # 1312 | # restart services & run post-sync script hook 1313 | startServices 1314 | runPostSyncHook 1315 | 1316 | 1317 | 1318 | ################################################# 1319 | # clone process end 1320 | ################################################# 1321 | 1322 | # may wait for user confirm (set in conf) 1323 | doCleanup 1324 | 1325 | END_TIMER=`date '+%s'` 1326 | PROCESS_TIME=$((END_TIMER - START_TIMER)) 1327 | doMsg "total clone process time : $((PROCESS_TIME / 60))m $((PROCESS_TIME % 60))s" "info" 1328 | 1329 | END_TIME=`date '+%Y-%m-%dT%H:%M:%S'` 1330 | if [ $WITH_WARNINGS = true ]; then 1331 | doMsg "ClonePi finished succesfully but with WARNINGS : $END_TIME" "msg" 1332 | else 1333 | doMsg "ClonePi finished succesfully : $END_TIME" "msg" 1334 | fi 1335 | echo 1336 | exit 0 1337 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # uninstall script for ClonePi 4 | # last updated @ v1.6.2 5 | # 6 | # - deletes clonepi command script 7 | # - copies config dir to /tmp 8 | # - deletes config dir 9 | 10 | 11 | # 12 | # helper functions 13 | # 14 | doMsg() 15 | { 16 | # msg, type 17 | case "$2" in 18 | warn) 19 | printf "WARNING: ${1}\n" 20 | echo 21 | ;; 22 | user-abort) 23 | printf "User aborted: ${1}\n" 24 | echo 25 | exit 0 26 | ;; 27 | error) 28 | printf "ERROR: ${1}\n" 29 | echo "Aborting!" 30 | echo 31 | exit 1 32 | ;; 33 | esac 34 | } 35 | 36 | 37 | # 38 | # config 39 | # 40 | INSTALL_DIR="/usr/local/sbin" 41 | CONF_DIR="/etc/clonepi" 42 | BAK_DIR="/tmp/clonepi-conf-bak" 43 | 44 | 45 | # exit if not root 46 | if [ `id -u` != 0 ]; then 47 | doMsg "The clonepi uninstaller needs to be run as root" "error" 48 | fi 49 | 50 | # 51 | # summarise and get user confirmation 52 | # 53 | echo 54 | echo "This will remove clonepi and its config files from your system." 55 | echo 56 | read -p "Continue with uninstall (yes|no)? " UI < /dev/tty 57 | if [ ! "$UI" = "y" -a ! "$UI" = "yes" ]; then 58 | doMsg "uninstall not confirmed" "user-abort" 59 | fi 60 | 61 | # 62 | # And uninstall 63 | # 64 | echo 65 | if [ -f ${INSTALL_DIR}/clonepi ]; then 66 | rm -f ${INSTALL_DIR}/clonepi 67 | if [ "$?" = 0 ]; then 68 | echo "Deleted ${INSTALL_DIR}/clonepi" 69 | else 70 | doMsg "could not delete ${INSTALL_DIR}/clonepi" "error" 71 | fi 72 | fi 73 | if [ -d ${CONF_DIR} ]; then 74 | rm -rf ${BAK_DIR} && mv ${CONF_DIR} ${BAK_DIR} 75 | if [ "$?" = 0 ]; then 76 | echo "Deleted ${CONF_DIR}" 77 | echo "A copy of the ${CONF_DIR} config dir has been placed at ${BAK_DIR}" 78 | echo 79 | else 80 | doMsg "could not delete ${CONF_DIR}" "error" 81 | fi 82 | fi 83 | exit 0 84 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.7.4 2 | --------------------------------------------------------------------------------