d942c909ec0ff78945ef214d83aea691bd66d545
[live-initramfs-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\", \"ext4\", \"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                         *.ext4)
262                                 SNAP_TYPE="ext4"
263                                 ;;
264
265                         *)
266                                 Error "unrecognized resync string"
267                                 ;;
268                 esac
269         elif [ -z "${SNAP_OUTPUT}" ]
270         then
271                 # Set target file based on image
272                 case "${SNAP_TYPE}" in
273                         cpio)
274                                 DEST="${MOUNTP}/live-sn.cpio.gz"
275                                 ;;
276
277                         squashfs|jffs2|ext2)
278                                 DEST="${MOUNTP}/live-sn.${SNAP_TYPE}"
279                                 ;;
280
281                         ext3)
282                                 DEST="${MOUNTP}/live-sn.ext2"
283                                 ;;
284
285                         ext4)
286                                 DEST="${MOUNTP}/live-sn.ext4"
287                                 ;;
288                 esac
289         else
290                 DEST="${SNAP_OUTPUT}"
291         fi
292 }
293
294 Validate_input ()
295 {
296         case "${SNAP_TYPE}" in
297                 cpio|squashfs|jffs2|ext2|ext3|ext4)
298                         ;;
299
300                 *)
301                         Error "invalid filesystem type \"${SNAP_TYPE}\""
302                         ;;
303         esac
304
305         if [ ! -d "${SNAP_COW}" ]
306         then
307                 Error "${SNAP_COW} is not a directory"
308         fi
309
310         if [ "$(id -u)" -ne 0 ]
311         then
312                 Error "you are not root"
313         fi
314 }
315
316 Mount_device ()
317 {
318         case "${SNAP_DEV}" in
319                 "")
320                         # create a temp
321                         mount -t tmpfs -o rw tmpfs "${MOUNTP}"
322                         ;;
323
324                 *)
325                         if [ -b "${SNAP_DEV}" ]
326                         then
327                                 try_mount "${SNAP_DEV}" "${MOUNTP}" rw
328                         fi
329                         ;;
330         esac
331 }
332
333 Entry_is_modified ()
334 {
335         # Returns true if file exists and it is also present in "cow" directory
336         # This means it is modified in respect to read-only media, so it deserve
337         # to be saved
338
339         entry="${1}"
340
341         if [ -e "${entry}" ] || [ -L "${entry}" ]
342         then
343                 if [ -e "${DEF_SNAP_COW}/${entry}" ] || [ -L "${DEF_SNAP_COW}/${entry}" ]
344                 then
345                         return 0
346                 fi
347         fi
348         return 1
349 }
350
351 Do_filelist ()
352 {
353         # BUGS: supports only cpio.gz types, and does not handle deleted files yet
354
355         TMP_FILELIST=$1
356         if [ -f "${SNAP_LIST}" ]
357         then
358                 # Generate include list removing empty and commented lines
359                 for entry in $(sed -e '/^ *$/d' -e '/^#.*$/d' "${SNAP_LIST}")
360                 do
361                         if [ -d "${entry}" ]
362                         then
363                                 cd /
364                                 find "${entry}" | while read line
365                                 do
366                                         if Entry_is_modified "${line}"
367                                         then
368                                                 printf "%s\000" "${line}" >> "${TMP_FILELIST}"
369                                         fi
370                                 done
371                                 cd "${OLDPWD}"
372                         elif Entry_is_modified "${entry}"
373                         then
374                                 # if file exists and it is modified
375                                 printf "%s\000" "${entry}" >> "${TMP_FILELIST}"
376                         fi
377                 done
378
379                 if [ "${SNAP_COW}" = "${DEF_SNAP_COW}" ]
380                 then
381                         # Relative to rootfs
382                         echo "/"
383                 else
384                         # Mostly "/home"
385                         echo "${SNAP_COW}"
386                 fi
387         else
388                 cd "${SNAP_COW}"
389                 find . -path '*.wh.*' -prune -o -print0 >> "${TMP_FILELIST}"
390                 cd "${OLDPWD}"
391                 echo "${SNAP_COW}"
392         fi
393 }
394
395 Do_snapshot ()
396 {
397         TMP_FILELIST=$(mktemp -p "${SAFE_TMPDIR}" "${TMP_FILELIST}.XXXXXX")
398
399         case "${SNAP_TYPE}" in
400                 squashfs)
401                         echo ".${TMP_FILELIST}" > "${TMP_FILELIST}"
402                         # Removing whiteheads of unionfs
403                         cd "${SNAP_COW}"
404                         find . -name '*.wh.*' >> "${TMP_FILELIST}"
405
406                         if [ -e "${EXCLUDE_LIST}" ]
407                         then
408                                 # Add explicitly excluded files
409                                 grep -v '^#.*$' "${EXCLUDE_LIST}" | grep -v '^ *$' >> "${TMP_FILELIST}"
410                         fi
411
412                         cd "${OLDPWD}"
413                         mksquashfs "${SNAP_COW}" "${DEST}" -ef "${TMP_FILELIST}"
414                         ;;
415
416                 cpio)
417                         WORKING_DIR=$(Do_filelist "${TMP_FILELIST}")
418                         cd "${WORKING_DIR}"
419                         if [ -e "${EXCLUDE_LIST}" ]
420                         then
421                                 # Convert \0 to \n and tag existing (rare but possible) \n in filenames,
422                                 # this to let grep -F -v do a proper work in filtering out
423                                 cat "${TMP_FILELIST}" | \
424                                         tr '\n' '\1' | \
425                                         tr '\0' '\n' | \
426                                         grep -F -v -f "${EXCLUDE_LIST}" | \
427                                         tr '\n' '\0' | \
428                                         tr '\1' '\n' | \
429                                         cpio --quiet -o0 -H newc | \
430                                         gzip -9c > "${DEST}" || exit 1
431                         else
432                                 cat "${TMP_FILELIST}" | \
433                                         cpio --quiet -o0 -H newc | \
434                                         gzip -9c > "${DEST}" || exit 1
435                         fi
436                         cd "${OLDPWD}"
437                         ;;
438
439                 # ext2|ext3|ext4 and jffs2 does not easily support an exclude list; files
440                 # should be copied to another directory in order to filter content
441                 ext2|ext3|ext4)
442                         DU_DIM="$(du -ks ${SNAP_COW} | cut -f1)"
443                         REAL_DIM="$(expr ${DU_DIM} + ${DU_DIM} / 20)" # Just 5% more to be sure, need something more sophistcated here...
444                         genext2fs --size-in-blocks=${REAL_DIM} --reserved-percentage=0 --root="${SNAP_COW}" "${DEST}"
445                         ;;
446
447                 jffs2)
448                         mkfs.jffs2 --root="${SNAP_COW}" --output="${DEST}"
449                         ;;
450         esac
451
452         if [ -f "${TMP_FILELIST}" ]
453         then
454                 rm -f "${TMP_FILELIST}"
455         fi
456 }
457
458 Clean ()
459 {
460         if [ -z "${SNAP_RESYNC_STRING}" ] && echo "${DEST}" | grep -q "${MOUNTP}"
461         then
462                 echo "${DEST} is present on ${MOUNTP}, therefore no automatic unmounting the latter." > /dev/null 1>&2
463         else
464                 umount "${MOUNTP}"
465                 rmdir "${MOUNTP}"
466         fi
467 }
468
469 Warn_user ()
470 {
471         if [ -z "${SNAP_RESYNC_STRING}" ]
472         then
473                 case ${SNAP_TYPE} in
474                         cpio|ext2|ext3|ext4)
475                                 echo "Please move ${DEST} (if is not already in it)" > /dev/null 1>&2
476                                 echo "in a supported writable partition (e.g ext3, vfat)." > /dev/null 1>&2
477                                 ;;
478
479                         squashfs)
480                                 echo "To use ${DEST} you need to rebuild your media or add it" > /dev/null 1>&2
481                                 echo "to your multisession disc under the \"/live\" directory." > /dev/null 1>&2
482                                 ;;
483
484                         jffs2)
485                                 echo "Please cat or flashcp ${DEST} to your partition in order to start using it." > /dev/null 1>&2
486                                 ;;
487                 esac
488
489                 if grep -qv persistent /proc/cmdline
490                 then
491                         echo "Remember to boot this live system with \"persistent\" specified at boot prompt." > /dev/null 1>&2
492                 fi
493         fi
494 }
495
496 Main ()
497 {
498         Parse_args "${@}"
499         Defaults
500         Validate_input
501         trap 'Clean' EXIT
502         Mount_device
503         Do_snapshot
504         Warn_user
505 }
506
507 Main "${@:-}"