Sync with Debian's upstream/3.0_a27 (new log file name, persistency reworked,...)
[live-boot-grml.git] / bin / live-snapshot
1 #!/bin/sh
2
3 # live-snapshot - utility to manage Debian Live systems snapshots
4 #
5 #   This program mounts a device (fallback to /tmpfs under $MOUNTP
6 #   and saves the /live/overlway (or a different directory) filesystem in it
7 #   for reuse in another live-boot session.
8 #   Look at the manpage for more informations.
9 #
10 # Copyright (C) 2006-2011 Marco Amadori <marco.amadori@gmail.com>
11 # Copyright (C) 2008 Chris Lamb <chris@chris-lamb.co.uk>
12 #
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 #
26 # The complete text of the GNU General Public License
27 # can be found in /usr/share/common-licenses/GPL-3 file.
28
29 # declare here two vars from /etc/live.conf because of "set -u"
30 ROOTSNAP=""
31 HOMESNAP=""
32
33 if [ -n "${LIVE_SNAPSHOT_CHECK_UNBOUND}" ]
34 then
35         set -eu
36 else
37         set -e
38 fi
39
40 . /usr/share/initramfs-tools/scripts/live-helpers
41
42 LIVE_CONF="/etc/live/boot.d/snapshot.conf"
43
44 if [ -r "${LIVE_CONF}" ]
45 then
46         . "${LIVE_CONF}"
47 fi
48
49 export USERNAME USERFULLNAME HOSTNAME
50
51 EXECUTABLE="${0}"
52 PROGRAM=$(basename "${EXECUTABLE}")
53
54 # Needs to be available at run and reboot time
55 SAFE_TMPDIR="/live"
56
57 # Permits multiple runs
58 MOUNTP="$(mktemp -d -p ${SAFE_TMPDIR} live-snapshot-mnt.XXXXXX)"
59 DEST="${MOUNTP}/live-sn.cpio.gz"
60 DEF_SNAP_COW="/live/overlay"
61 TMP_FILELIST="${PROGRAM}.list"
62
63 # Command line defaults and declarations
64 SNAP_COW="${DEF_SNAP_COW}"
65 SNAP_DEV=""
66 SNAP_MNT=""
67 SNAP_OUTPUT=""
68 SNAP_RESYNC_STRING=""
69 SNAP_TYPE="cpio"
70 SNAP_LIST="/etc/live-snapshot.list"
71 EXCLUDE_LIST="/etc/live-snapshot.exclude_list"
72
73 Error ()
74 {
75         echo "${PROGRAM}: error:" ${@}
76         exit 1
77 }
78
79 panic ()
80 {
81         Error ${@}
82 }
83
84 Header ()
85 {
86         echo "${PROGRAM} - utility to perform snapshots of Debian Live systems"
87         echo
88         echo "usage: ${PROGRAM} [-c|--cow DIRECTORY] [-d|--device DEVICE] [-o|--output FILE] [-t|--type TYPE]"
89         echo "       ${PROGRAM} [-r|--resync-string STRING]"
90         echo "       ${PROGRAM} [-f|--refresh]"
91         echo "       ${PROGRAM} [-h|--help]"
92         echo "       ${PROGRAM} [-u|--usage]"
93         echo "       ${PROGRAM} [-v|--version]"
94 }
95
96 Help ()
97 {
98         Header
99
100         echo
101         echo "Options:"
102         echo "  -c, --cow: copy on write directory (default: ${SNAP_COW})."
103         echo "  -d, --device: output snapshot device (default: ${SNAP_DEV:-auto})."
104         echo "  -o, --output: output image file (default: ${DEST})."
105         echo "  -r, --resync-string: internally used to resync previous made snapshots."
106         echo "  -f, --refresh: try to sync a running snapshot."
107         echo "  -t, --type: snapshot filesystem type. Options: \"squashfs\", \"ext2\", \"ext3\", \"ext4\", \"jffs2\" or \"cpio\".gz archive (default: ${SNAP_TYPE})"
108         echo
109         echo "Look at live-snapshot(1) man page for more information."
110
111         exit 0
112 }
113
114 Usage ()
115 {
116         Header
117
118         echo
119         echo "Try \"${PROGRAM} --help\" for more information."
120
121         exit 0
122 }
123
124 Version ()
125 {
126         echo "${PROGRAM}"
127         echo
128         echo "Copyright (C) 2006-2011 Marco Amadori <marco.amadori@gmail.com>"
129         echo "Copyright (C) 2008 Chris Lamb <chris@chris-lamb.co.uk>"
130         echo
131         echo "This program is free software; you can redistribute it and/or modify"
132         echo "it under the terms of the GNU General Public License as published by"
133         echo "the Free Software Foundation; either version 3 of the License, or"
134         echo "(at your option) any later version."
135         echo
136         echo "This program is distributed in the hope that it will be useful,"
137         echo "but WITHOUT ANY WARRANTY; without even the implied warranty of"
138         echo "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the"
139         echo "GNU General Public License for more details."
140         echo
141         echo "You should have received a copy of the GNU General Public License"
142         echo "along with this program; if not, write to the Free Software"
143         echo "Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA"
144         echo
145         echo "The complete text of the GNU General Public License"
146         echo "can be found in /usr/share/common-licenses/GPL-3 file."
147         echo
148         echo "Homepage: <http://live.debian.net/>"
149
150         exit 0
151 }
152
153 Try_refresh ()
154 {
155         FOUND=""
156         if [ -n "${ROOTSNAP}" ]; then
157                 "${EXECUTABLE}" --resync-string="${ROOTSNAP}"
158                 FOUND="Yes"
159         fi
160
161         if [ -n "${HOMESNAP}" ]; then
162                 "${EXECUTABLE}" --resync-string="${HOMESNAP}"
163                 FOUND="Yes"
164         fi
165
166         if [ -z "${FOUND}" ]
167         then
168                 echo "No autoconfigured snapshots found at boot;" > /dev/null 1>&2
169                 echo "(no resync string in ${LIVE_CONF})." > /dev/null 1>&2
170                 exit 1
171         fi
172 }
173
174 Parse_args ()
175 {
176         # Parse command line
177         ARGS="${*}"
178         ARGUMENTS="$(getopt --longoptions cow:,device:,output,resync-string:,refresh,type:,help,usage,version --name=${PROGRAM} --options c:d:o:t:r:fhuv --shell sh -- ${ARGS})"
179
180         eval set -- "${ARGUMENTS}"
181
182         while true
183         do
184                 case "${1}" in
185                         -c|--cow)
186                                 SNAP_COW="${2}"
187                                 shift 2
188                                 ;;
189
190                         -d|--device)
191                                 SNAP_DEV="${2}"
192                                 shift 2
193                                 ;;
194
195                         -o|--output)
196                                 SNAP_OUTPUT="${2}"
197                                 shift 2
198                                 ;;
199
200                         -t|--type)
201                                 SNAP_TYPE="${2}"
202                                 shift 2
203                                 ;;
204
205                         -r|--resync-string)
206                                 SNAP_RESYNC_STRING="${2}"
207                                 break
208                                 ;;
209
210                         -f|--refresh)
211                                 Try_refresh
212                                 exit 0
213                                 ;;
214
215                         -h|--help)
216                                 Help
217                                 ;;
218
219                         -u|--usage)
220                                 Usage
221                                 ;;
222
223                         -v|--version)
224                                 Version
225                                 ;;
226
227                         --)
228                                 shift
229                                 break
230                                 ;;
231
232                         *)
233                                 Error "internal error."
234                                 ;;
235
236                 esac
237         done
238 }
239
240 Defaults ()
241 {
242         # Parse resync string
243         if [ -n "${SNAP_RESYNC_STRING}" ]
244         then
245                 SNAP_COW=$(echo "${SNAP_RESYNC_STRING}" | sed -r -e 's#^([^:]*).*$#'"${DEF_SNAP_COW}"'\1#')
246                 SNAP_DEV=$(echo "${SNAP_RESYNC_STRING}" | cut -f2 -d ':')
247                 SNAP_MNT=$(echo "${SNAP_RESYNC_STRING}" | cut -f3 -d ':')
248                 DEST="${MOUNTP}/${SNAP_MNT}"
249
250                 case "${SNAP_MNT}" in
251                         *.cpio.gz)
252                                 SNAP_TYPE="cpio"
253                                 ;;
254
255                         *.squashfs)
256                                 SNAP_TYPE="squashfs"
257                                 ;;
258
259                         *.jffs2)
260                                 SNAP_TYPE="jffs2"
261                                 ;;
262
263                         *.ext2|*.ext3)
264                                 SNAP_TYPE="ext2"
265                                 ;;
266
267                         "")
268                                 SNAP_TYPE="whole_partition"
269                                 ;;
270
271                         *.ext4)
272                                 SNAP_TYPE="ext4"
273                                 ;;
274
275                         *)
276                                 Error "unrecognized resync string"
277                                 ;;
278                 esac
279         elif [ -z "${SNAP_OUTPUT}" ]
280         then
281                 # Set target file based on image
282                 case "${SNAP_TYPE}" in
283                         cpio)
284                                 DEST="${MOUNTP}/live-sn.cpio.gz"
285                                 ;;
286
287                         squashfs|jffs2|ext2)
288                                 DEST="${MOUNTP}/live-sn.${SNAP_TYPE}"
289                                 ;;
290
291                         ext3)
292                                 DEST="${MOUNTP}/live-sn.ext2"
293                                 ;;
294
295                         ext4)
296                                 DEST="${MOUNTP}/live-sn.ext4"
297                                 ;;
298                 esac
299         else
300                 DEST="${SNAP_OUTPUT}"
301         fi
302 }
303
304 Validate_input ()
305 {
306         case "${SNAP_TYPE}" in
307                 cpio|squashfs|jffs2|ext2|ext3|ext4|whole_partition)
308                         ;;
309
310                 *)
311                         Error "invalid filesystem type \"${SNAP_TYPE}\""
312                         ;;
313         esac
314
315         if [ ! -d "${SNAP_COW}" ]
316         then
317                 Error "${SNAP_COW} is not a directory"
318         fi
319
320         if [ "$(id -u)" -ne 0 ]
321         then
322                 Error "you are not root"
323         fi
324 }
325
326 Mount_device ()
327 {
328         case "${SNAP_DEV}" in
329                 "")
330                         # create a temp
331                         mount -t tmpfs -o rw tmpfs "${MOUNTP}"
332                         ;;
333
334                 *)
335                         if [ -b "${SNAP_DEV}" ]
336                         then
337                                 try_mount "${SNAP_DEV}" "${MOUNTP}" rw
338                         fi
339                         ;;
340         esac
341 }
342
343 Entry_is_modified ()
344 {
345         # Returns true if file exists and it is also present in "cow" directory
346         # This means it is modified in respect to read-only media, so it deserve
347         # to be saved
348
349         entry="${1}"
350
351         if [ -e "${entry}" ] || [ -L "${entry}" ]
352         then
353                 if [ -e "${SNAP_COW}/${entry}" ] || [ -L "${SNAP_COW}/${entry}" ]
354                 then
355                         return 0
356                 fi
357         fi
358         return 1
359 }
360
361 Do_filelist ()
362 {
363         # BUGS: does not handle deleted files yet
364         TMP_FILELIST=$1
365
366         if [ -f "${SNAP_LIST}" ]
367         then
368                 # if SNAP_COW == /live/overlay/home, SNAP_RW = /home
369                 SNAP_RW=$(echo "${SNAP_COW}" | sed -e "s|${DEF_SNAP_COW}||g")
370                 if [ -z "${SNAP_RW}" ]
371                 then
372                         SNAP_RW="/"
373                 fi
374
375                 cd "${SNAP_RW}"
376                 # Generate include list removing empty and commented lines
377                 # and transforming paths to relatives
378                 for entry in $(sed -e '/^ *$/d' -e '/^#.*$/d' -e 's#^.*$#./&#' -e 's#/\+#/#g' "${SNAP_LIST}")
379                 do
380                         if [ -d "${entry}" ]
381                         then
382                                 find "${entry}" | while read line
383                                 do
384                                         if Entry_is_modified "${line}"
385                                         then
386                                                 printf "%s\000" "${line}" >> "${TMP_FILELIST}"
387                                         fi
388                                 done
389                         elif Entry_is_modified "${entry}"
390                         then
391                                 # if file exists and it is modified
392                                 printf "%s\000" "${entry}" >> "${TMP_FILELIST}"
393                         fi
394                 done
395                 cd "${OLDPWD}"
396
397                 # echo Working dir
398                 echo "${SNAP_RW}"
399         else
400                 cd "${SNAP_COW}"
401                 # removing whiteouts from list
402                 find . -path '*.wh.*' -prune -o -print0 >> "${TMP_FILELIST}"
403                 cd "${OLDPWD}"
404                 # echo Working dir
405                 echo "${SNAP_COW}"
406         fi
407 }
408
409 Do_snapshot ()
410 {
411         TMP_FILELIST=$(mktemp -p "${SAFE_TMPDIR}" "${TMP_FILELIST}.XXXXXX")
412         if [ -e "${EXCLUDE_LIST}" ]
413         then
414                 # Create a TMP filelist removing empty lines (grep -f does not like them)
415                 # and comments (for speedup and LST)
416                 TMP_EXCLUDE_LIST=$(mktemp -p "${SAFE_TMPDIR}" "${PROGRAM}_excludelist.XXXXXX")
417                 grep -v '^#.*$' "${EXCLUDE_LIST}" | grep -v '^ *$' > "${TMP_EXCLUDE_LIST}"
418         fi
419
420         case "${SNAP_TYPE}" in
421                 squashfs)
422                         echo ".${TMP_FILELIST}" > "${TMP_FILELIST}"
423                         # Removing whiteheads of unionfs
424                         cd "${SNAP_COW}"
425                         find . -name '*.wh.*' >> "${TMP_FILELIST}"
426
427                         if [ -e "${EXCLUDE_LIST}" ]
428                         then
429                                 # Add explicitly excluded files
430                                 cat "${TMP_EXCLUDE_LIST}" >> "${TMP_FILELIST}"
431                         fi
432
433                         cd "${OLDPWD}"
434                         mksquashfs "${SNAP_COW}" "${DEST}" -ef "${TMP_FILELIST}"
435                         ;;
436
437                 cpio|whole_partition)
438                         if [ "${SNAP_TYPE}" = "cpio" ]
439                         then
440                                 COPY_CMD="cpio --quiet -o0 -H newc | gzip -9c > ${DEST}"
441                         else
442                                 COPY_CMD="cpio --quiet -pumd0 ${DEST}/"
443                         fi
444
445                         WORKING_DIR=$(Do_filelist "${TMP_FILELIST}")
446                         cd "${WORKING_DIR}"
447                         if [ -e "${EXCLUDE_LIST}" ]
448                         then
449
450                                 # Convert \0 to \n and tag existing (rare but possible) \n in filenames,
451                                 # this to let grep -F -v do a proper work in filtering out
452                                 cat "${TMP_FILELIST}" | \
453                                         tr '\n' '\1' | \
454                                         tr '\0' '\n' | \
455                                         grep -F -v -f "${TMP_EXCLUDE_LIST}" | \
456                                         tr '\n' '\0' | \
457                                         tr '\1' '\n' | \
458                                         eval $COPY_CMD || exit 1
459                         else
460                                 cat "${TMP_FILELIST}" | \
461                                         eval $COPY_CMD || exit 1
462                         fi
463                         cd "${OLDPWD}"
464                         ;;
465
466                 # ext2|ext3|ext4 and jffs2 does not easily support an exclude list; files
467                 # should be copied to another directory in order to filter content
468                 ext2|ext3|ext4)
469                         DU_DIM="$(du -ks ${SNAP_COW} | cut -f1)"
470                         REAL_DIM="$(expr ${DU_DIM} + ${DU_DIM} / 20)" # Just 5% more to be sure, need something more sophistcated here...
471                         genext2fs --size-in-blocks=${REAL_DIM} --reserved-percentage=0 --root="${SNAP_COW}" "${DEST}"
472                         ;;
473
474                 jffs2)
475                         mkfs.jffs2 --root="${SNAP_COW}" --output="${DEST}"
476                         ;;
477         esac
478
479         # Remove temporary file lists
480         for filelist in "${TMP_FILELIST}" "${TMP_EXCLUDE_LIST}"
481         do
482                 if [ -f "${filelist}" ]
483                 then
484                         rm -f "${filelist}"
485                 fi
486         done
487 }
488
489 Clean ()
490 {
491         if [ -z "${SNAP_RESYNC_STRING}" ] && echo "${DEST}" | grep -q "${MOUNTP}"
492         then
493                 echo "${DEST} is present on ${MOUNTP}, therefore no automatic unmounting the latter." > /dev/null 1>&2
494         else
495                 umount "${MOUNTP}"
496                 rmdir "${MOUNTP}"
497         fi
498 }
499
500 Warn_user ()
501 {
502         if [ -z "${SNAP_RESYNC_STRING}" ]
503         then
504                 case ${SNAP_TYPE} in
505                         cpio|ext2|ext3|ext4)
506                                 echo "Please move ${DEST} (if is not already in it)" > /dev/null 1>&2
507                                 echo "in a supported writable partition (e.g ext3, vfat)." > /dev/null 1>&2
508                                 ;;
509
510                         squashfs)
511                                 echo "To use ${DEST} you need to rebuild your media or add it" > /dev/null 1>&2
512                                 echo "to your multisession disc under the \"/live\" directory." > /dev/null 1>&2
513                                 ;;
514
515                         jffs2)
516                                 echo "Please cat or flashcp ${DEST} to your partition in order to start using it." > /dev/null 1>&2
517                                 ;;
518                 esac
519
520                 if grep -qv persistence /proc/cmdline
521                 then
522                         echo "Remember to boot this live system with \"persistence\" specified at boot prompt." > /dev/null 1>&2
523                 fi
524         fi
525 }
526
527 Main ()
528 {
529         Parse_args "${@}"
530         Defaults
531         Validate_input
532         trap 'Clean' EXIT
533         Mount_device
534         Do_snapshot
535         Warn_user
536 }
537
538 Main "${@:-}"