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