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