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