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