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