Release new version 0.11.0
[grml2usb.git] / grml2usb
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # pylint: disable-msg=C0302
4 """
5 grml2usb
6 ~~~~~~~~
7
8 This script installs a grml system (either a running system or ISO[s]) to a USB device
9
10 :copyright: (c) 2009, 2010, 2011 by Michael Prokop <mika@grml.org>
11 :license: GPL v2 or any later version
12 :bugreports: http://grml.org/bugs/
13
14 """
15
16 from optparse import OptionParser
17 from inspect import isroutine, isclass
18 import datetime, logging, os, re, subprocess, sys, tempfile, time, os.path
19 import fileinput
20 import glob
21 import uuid
22 import struct
23
24 # global variables
25 PROG_VERSION = "0.9.31"
26 MOUNTED = set()  # register mountpoints
27 TMPFILES = set() # register tmpfiles
28 DATESTAMP = time.mktime(datetime.datetime.now().timetuple()) # unique identifier for syslinux.cfg
29 GRML_FLAVOURS = set() # which flavours are being installed?
30 GRML_DEFAULT = None
31 UUID = None
32 SYSLINUX_LIBS = "/usr/lib/syslinux/"
33
34 def syslinux_warning(option, opt, value, opt_parser):
35     """A helper function for printing a warning about deprecated option
36     """
37     # pylint: disable-msg=W0613
38     sys.stderr.write("Note: the --syslinux option is deprecated as syslinux "
39                      "is grml2usb's default. Continuing anyway.\n")
40     setattr(opt_parser.values, option.dest, True)
41
42 # if grub option is set, unset syslinux option
43 def grub_option(option, opt, value, opt_parser):
44     """A helper function adjusting other option values
45     """
46     # pylint: disable-msg=W0613
47     setattr(opt_parser.values, option.dest, True)
48     setattr(opt_parser.values, 'syslinux', False)
49
50 # cmdline parsing
51 USAGE = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/sdX#>\n\
52 \n\
53 %prog installs grml ISO[s] to an USB device to be able to boot from it.\n\
54 Make sure you have at least one grml ISO or a running grml system (/live/image),\n\
55 grub or syslinux and root access.\n\
56 \n\
57 Run %prog --help for usage hints, further information via: man grml2usb"
58
59 # pylint: disable-msg=C0103
60 # pylint: disable-msg=W0603
61 parser = OptionParser(usage=USAGE)
62 parser.add_option("--bootoptions", dest="bootoptions",
63                   action="append", type="string",
64                   help="use specified bootoptions as default")
65 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
66                   help="do not copy files but just install a bootloader")
67 parser.add_option("--copy-only", dest="copyonly", action="store_true",
68                   help="copy files only but do not install bootloader")
69 parser.add_option("--dry-run", dest="dryrun", action="store_true",
70                   help="avoid executing commands")
71 parser.add_option("--fat16", dest="fat16", action="store_true",
72                   help="format specified partition with FAT16")
73 parser.add_option("--force", dest="force", action="store_true",
74                   help="force any actions requiring manual interaction")
75 parser.add_option("--grub", dest="grub", action="callback",
76                   callback=grub_option,
77                   help="install grub bootloader instead of (default) syslinux")
78 parser.add_option("--grub-mbr", dest="grubmbr", action="store_true",
79                   help="install grub into MBR instead of (default) PBR")
80 parser.add_option("--mbr-menu", dest="mbrmenu", action="store_true",
81                   help="enable interactive boot menu in MBR")
82 parser.add_option("--quiet", dest="quiet", action="store_true",
83                   help="do not output anything but just errors on console")
84 parser.add_option("--remove-bootoption", dest="removeoption", action="append",
85                   help="regex for removing existing bootoptions")
86 parser.add_option("--skip-addons", dest="skipaddons", action="store_true",
87                   help="do not install /boot/addons/ files")
88 parser.add_option("--skip-grub-config", dest="skipgrubconfig", action="store_true",
89                   help="skip generation of grub configuration files")
90 parser.add_option("--skip-mbr", dest="skipmbr", action="store_true",
91                   help="do not install a master boot record (MBR) on the device")
92 parser.add_option("--skip-syslinux-config", dest="skipsyslinuxconfig", action="store_true",
93                   help="skip generation of syslinux configuration files")
94 parser.add_option("--syslinux", dest="syslinux", action="callback", default=True,
95                   callback=syslinux_warning,
96                   help="install syslinux bootloader (deprecated as it's the default)")
97 parser.add_option("--syslinux-mbr", dest="syslinuxmbr", action="store_true",
98                   help="install syslinux master boot record (MBR) instead of default")
99 parser.add_option("--verbose", dest="verbose", action="store_true",
100                   help="enable verbose mode")
101 parser.add_option("-v", "--version", dest="version", action="store_true",
102                   help="display version and exit")
103 (options, args) = parser.parse_args()
104
105
106 GRML2USB_BASE = '/usr/share/grml2usb'
107 if not os.path.isdir(GRML2USB_BASE):
108     GRML2USB_BASE = os.path.dirname(os.path.realpath(__file__))
109
110
111 class CriticalException(Exception):
112     """Throw critical exception if the exact error is not known but fatal."
113
114     @Exception: message"""
115     pass
116
117
118 # The following two functions help to operate on strings as
119 # array (list) of bytes (octets). In Python 3000, the bytes
120 # datatype will need to be used. This is intended for using
121 # with manipulation of files on the octet level, like shell
122 # arrays, e.g. in MBR creation.
123
124
125 def array2string(*a):
126     """Convert a list of integers [0;255] to a string."""
127     return struct.pack("%sB" % len(a), *a)
128
129
130 def string2array(s):
131     """Convert a (bytes) string into a list of integers."""
132     return struct.unpack("%sB" % len(s), s)
133
134
135 def cleanup():
136     """Cleanup function to make sure there aren't any mounted devices left behind.
137     """
138
139     logging.info("Cleaning up before exiting...")
140     proc = subprocess.Popen(["sync"])
141     proc.wait()
142
143     try:
144         for device in MOUNTED:
145             unmount(device, "")
146         for tmpfile in TMPFILES:
147             os.unlink(tmpfile)
148     # ignore: RuntimeError: Set changed size during iteration
149     except RuntimeError:
150         logging.debug('caught expection RuntimeError, ignoring')
151
152
153 def register_tmpfile(path):
154     """
155     register tmpfile
156     """
157
158     TMPFILES.add(path)
159
160
161 def unregister_tmpfile(path):
162     """
163     remove registered tmpfile
164     """
165
166     try:
167         TMPFILES.remove(path)
168     except KeyError:
169         pass
170
171
172 def register_mountpoint(target):
173     """register specified target in a set() for handling clean exiting
174
175     @target: destination target of mountpoint
176     """
177
178     MOUNTED.add(target)
179
180
181 def unregister_mountpoint(target):
182     """unregister specified target in a set() for handling clean exiting
183
184     @target: destination target of mountpoint
185     """
186
187     if target in MOUNTED:
188         MOUNTED.remove(target)
189
190
191 def get_function_name(obj):
192     """Helper function for use in execute() to retrive name of a function
193
194     @obj: the function object
195     """
196     if not (isroutine(obj) or isclass(obj)):
197         obj = type(obj)
198     return obj.__module__ + '.' + obj.__name__
199
200
201 def execute(f, *exec_arguments):
202     """Wrapper for executing a command. Either really executes
203     the command (default) or when using --dry-run commandline option
204     just displays what would be executed."""
205     # usage: execute(subprocess.Popen, (["ls", "-la"]))
206     if options.dryrun:
207         # pylint: disable-msg=W0141
208         logging.debug('dry-run only: %s(%s)', get_function_name(f), ', '.join(map(repr, exec_arguments)))
209     else:
210         # pylint: disable-msg=W0142
211         return f(*exec_arguments)
212
213
214 def is_exe(fpath):
215     """Check whether a given file can be executed
216
217     @fpath: full path to file
218     @return:"""
219     return os.path.exists(fpath) and os.access(fpath, os.X_OK)
220
221
222 def which(program):
223     """Check whether a given program is available in PATH
224
225     @program: name of executable"""
226     fpath = os.path.split(program)[0]
227     if fpath:
228         if is_exe(program):
229             return program
230     else:
231         for path in os.environ["PATH"].split(os.pathsep):
232             exe_file = os.path.join(path, program)
233             if is_exe(exe_file):
234                 return exe_file
235
236     return None
237
238
239 def get_defaults_file(iso_mount, flavour, name):
240     """get the default file for syslinux
241     """
242     bootloader_dirs = ['/boot/isolinux/', '/boot/syslinux/']
243     for directory in bootloader_dirs:
244         for name in name, \
245         "%s_%s" % (get_flavour_filename(flavour), name):
246             if os.path.isfile(iso_mount + directory + name):
247                 return (directory, name)
248     return ('','')
249
250 def search_file(filename, search_path='/bin' + os.pathsep + '/usr/bin'):
251     """Given a search path, find file
252
253     @filename: name of file to search for
254     @search_path: path where searching for the specified filename"""
255     file_found = 0
256     paths = search_path.split(os.pathsep)
257     current_dir = '' # make pylint happy :)
258
259     def match_file(cwd):
260         """Helper function ffor testing if specified file exists in cwd
261
262         @cwd: current working directory
263         """
264         return  os.path.exists(os.path.join(cwd, filename))
265
266     for path in paths:
267         current_dir = path
268         if match_file(current_dir):
269             file_found = 1
270             break
271         # pylint: disable-msg=W0612
272         for current_dir, directories, files in os.walk(path):
273             if match_file(current_dir):
274                 file_found = 1
275                 break
276     if file_found:
277         return os.path.abspath(os.path.join(current_dir, filename))
278     else:
279         return None
280
281
282 def check_uid_root():
283     """Check for root permissions"""
284     if not os.geteuid()==0:
285         sys.exit("Error: please run this script with uid 0 (root).")
286
287
288 def mkfs_fat16(device):
289     """Format specified device with VFAT/FAT16 filesystem.
290
291     @device: partition that should be formated"""
292
293     if options.dryrun:
294         logging.info("Would execute mkfs.vfat -F 16 %s now.", device)
295         return 0
296
297     logging.info("Formating partition with fat16 filesystem")
298     logging.debug("mkfs.vfat -F 16 %s", device)
299     proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
300     proc.wait()
301     if proc.returncode != 0:
302         raise CriticalException("error executing mkfs.vfat")
303
304
305 def generate_main_grub2_config(grml_flavour, bootoptions):
306     """Generate grub2 configuration for use via grub.cfg
307
308     @grml_flavour: name of grml flavour the configuration should be generated for
309     @bootoptions: additional bootoptions that should be used by default"""
310
311     local_datestamp = DATESTAMP
312
313     return("""\
314 ## main grub2 configuration - generated by grml2usb [main config generated at: %(local_datestamp)s]
315 set default=0
316 set timeout=10
317
318 insmod fat
319
320 if loadfont /boot/grub/ascii.pf2 ; then
321    insmod png
322    set gfxmode=640x480
323    insmod gfxterm
324    insmod vbe
325    if terminal_output gfxterm ; then true ; else
326     # For backward compatibility with versions of terminal.mod that don't
327     # understand terminal_output
328     terminal gfxterm
329    fi
330 fi
331
332 if background_image /boot/grub/grml.png ; then
333   set color_normal=black/black
334   set color_highlight=red/black
335 else
336   set menu_color_normal=white/black
337   set menu_color_highlight=black/yellow
338 fi
339
340 menuentry "%(grml_flavour)s (default)" {
341     set gfxpayload=1024x768x16,1024x768
342     linux   /boot/release/%(flavour_filename)s/linux26 apm=power-off quiet boot=live nomce live-media-path=/live/%(grml_flavour)s/ bootid=%(uid)s %(bootoptions)s
343     initrd  /boot/release/%(flavour_filename)s/initrd.gz
344 }
345
346 menuentry "Memory test (memtest86+)" {
347     linux16   /boot/addons/memtest
348 }
349
350 menuentry "Boot Grub (all in one image)" {
351     linux   /boot/addons/memdisk
352     initrd  /boot/addons/allinone.img
353 }
354
355 menuentry "Boot FreeDOS" {
356     linux   /boot/addons/memdisk
357     initrd  /boot/addons/balder10.imz
358 }
359
360 if [ ${iso_path} ] ; then
361     # assume loopback.cfg boot
362     if [ -e /boot/addons/bsd4grml/loopback.0 ] ; then
363         # bsd4grml 20100815 and later
364         menuentry "Boot MirOS bsd4grml" {
365             multiboot /boot/addons/bsd4grml/ldbsd.com
366             module /boot/addons/bsd4grml/bsd.rd bsd
367             module /boot/addons/bsd4grml/loopback.0 boot.cfg
368             module /boot/addons/bsd4grml/loopback.1 boot.1
369             module /boot/addons/bsd4grml/loopback.2 boot.2
370             module /boot/addons/bsd4grml/loopback.3 boot.3
371             module /boot/addons/bsd4grml/loopback.4 boot.4
372             module /boot/addons/bsd4grml/loopback.5 boot.5
373             module /boot/addons/bsd4grml/loopback.6 boot.6
374         }
375     else
376         # old bsd4grml
377         menuentry "Boot MirOS bsd4grml" {
378             multiboot /boot/addons/bsd4grml/ldbsd.com
379             module /boot/addons/bsd4grml/bsd.rd bsd.rd
380             module /boot/addons/bsd4grml/boot.cfg boot.cfg
381             module /boot/addons/bsd4grml/boot.1 boot.1
382             module /boot/addons/bsd4grml/boot.2 boot.2
383             module /boot/addons/bsd4grml/boot.3 boot.3
384             module /boot/addons/bsd4grml/boot.4 boot.4
385             module /boot/addons/bsd4grml/boot.5 boot.5
386         }
387     fi
388 else
389     # assume grub.cfg boot
390     menuentry "Boot MirOS bsd4grml" {
391         multiboot /boot/addons/bsd4grml/ldbsd.com
392         module /boot/addons/bsd4grml/bsd.rd bsd.rd
393         module /boot/addons/bsd4grml/boot.cfg boot.cfg
394         module /boot/addons/bsd4grml/boot.1 boot.1
395         module /boot/addons/bsd4grml/boot.2 boot.2
396         module /boot/addons/bsd4grml/boot.3 boot.3
397         module /boot/addons/bsd4grml/boot.4 boot.4
398         module /boot/addons/bsd4grml/boot.5 boot.5
399         module /boot/addons/bsd4grml/boot.6 boot.6
400     }
401 fi
402
403 menuentry "Boot OS of first partition on first disk" {
404     set root=(hd0,1)
405     chainloader +1
406 }
407
408 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp,
409        'flavour_filename': grml_flavour.replace('-', ''),
410        'uid': UUID, 'bootoptions': bootoptions } )
411
412
413 def generate_flavour_specific_grub2_config(grml_flavour, bootoptions):
414     """Generate grub2 configuration for use via grub.cfg
415
416     @grml_flavour: name of grml flavour the configuration should be generated for
417     @bootoptions: additional bootoptions that should be used by default"""
418
419     local_datestamp = DATESTAMP
420
421     return("""\
422 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
423 menuentry "%(grml_flavour)s            - boot in default mode" {
424     set gfxpayload=1024x768x16,1024x768
425     linux  /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ bootid=%(uid)s %(bootoptions)s
426     initrd /boot/release/%(flavour_filename)s/initrd.gz
427 }
428
429 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
430 menuentry "%(grml_flavour)s-persistent - enable persistency feature" {
431     set gfxpayload=1024x768x16,1024x768
432     linux  /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet persistent live-media-path=/live/%(grml_flavour)s/ bootid=%(uid)s %(bootoptions)s
433     initrd /boot/release/%(flavour_filename)s/initrd.gz
434 }
435
436 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
437 menuentry "%(grml_flavour)s2ram        - copy compressed grml file to RAM" {
438     set gfxpayload=1024x768x16,1024x768
439     linux  /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ toram=%(grml_flavour)s.squashfs bootid=%(uid)s %(bootoptions)s
440     initrd /boot/release/%(flavour_filename)s/initrd.gz
441 }
442
443 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
444 menuentry "%(grml_flavour)s-debug      - enable debugging options" {
445     set gfxpayload=1024x768x16,1024x768
446     linux /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ debug bootid=%(uid)s initcall_debug %(bootoptions)s
447     initrd /boot/release/%(flavour_filename)s/initrd.gz
448 }
449
450 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
451 menuentry "%(grml_flavour)s-x          - start X Window System" {
452     set gfxpayload=1024x768x16,1024x768
453     linux  /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ startx=wm-ng bootid=%(uid)s %(bootoptions)s
454     initrd /boot/release/%(flavour_filename)s/initrd.gz
455 }
456
457 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
458 menuentry "%(grml_flavour)s-nofb       - disable framebuffer" {
459     linux  /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ vga=normal video=ofonly bootid=%(uid)s %(bootoptions)s
460     initrd /boot/release/%(flavour_filename)s/initrd.gz
461 }
462
463 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
464 menuentry "%(grml_flavour)s-failsafe   - disable hardware detection" {
465     linux /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ vga=normal noautoconfig atapicd noapic noacpi acpi=off nomodules nofirewire noudev nousb nohotplug noapm nopcmcia nosmp maxcpus=0 noscsi noagp nodma ide=nodma noswap nofstab nosound nogpm nosyslog nodhcp nocpu nodisc nomodem xmodule=vesa noraid nolvm noresume selinux=0 edd=off pci=nomsi bootid=%(uid)s %(bootoptions)s
466     initrd /boot/release/%(flavour_filename)s/initrd.gz
467 }
468
469 ## flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
470 menuentry "%(grml_flavour)s-forensic   - do not touch harddisks during hw recognition" {
471     set gfxpayload=1024x768x16,1024x768
472     linux /boot/release/%(flavour_filename)s/linux26 apm=power-off boot=live nomce quiet live-media-path=/live/%(grml_flavour)s/ nofstab noraid nolvm noautoconfig noswap raid=noautodetect forensic readonly bootid=%(uid)s %(bootoptions)s
473     initrd /boot/release/%(flavour_filename)s/initrd.gz
474 }
475
476 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp,
477        'flavour_filename': grml_flavour.replace('-', ''),
478        'uid': UUID, 'bootoptions': bootoptions } )
479
480 def generate_isolinux_splash(grml_flavour):
481     """Generate bootsplash for isolinux/syslinux
482
483     @grml_flavour: name of grml flavour the configuration should be generated for"""
484
485     grml_name = grml_flavour
486
487     return("""\
488 \ f17\f\18/boot/syslinux/logo.16
489
490 Some information and boot options available via keys F2 - F10. http://grml.org/
491 %(grml_name)s
492 """ % {'grml_name': grml_name} )
493
494
495 def generate_main_syslinux_config(*arg):
496     """Generate main configuration for use in syslinux.cfg
497
498     @*arg: just for backward compatibility"""
499     # pylint: disable-msg=W0613
500     # remove warning about unused arg
501
502     return("""\
503 label -
504 menu label Default boot modes:
505 menu disable
506 include defaults.cfg
507
508 menu end
509 menu separator
510
511 # flavours:
512 label -
513 menu label Additional boot entries for:
514 menu disable
515 include additional.cfg
516
517 menu separator
518 include options.cfg
519 include addons.cfg
520
521 label help
522   include promptname.cfg
523   config prompt.cfg
524   text help
525                                         Jump to old style isolinux prompt
526                                         featuring further information
527                                         regarding available boot options.
528   endtext
529
530
531 include hiddens.cfg
532 """)
533
534
535 def generate_flavour_specific_syslinux_config(grml_flavour):
536     """Generate flavour specific configuration for use in syslinux.cfg
537
538     @grml_flavour: name of grml flavour the configuration should be generated for"""
539
540
541     return("""\
542 menu begin grml %(grml_flavour)s
543     menu title %(display_name)s
544     label mainmenu
545     menu label ^Back to main menu...
546     menu exit
547     menu separator
548     # include config for boot parameters from disk
549     include %(grml_flavour)s_grml.cfg
550     menu hide
551 menu end
552 """ % {'grml_flavour': grml_flavour, 'display_name' : grml_flavour.replace('_', '-') } )
553
554
555 def install_grub(device):
556     """Install grub on specified device.
557
558     @mntpoint: mountpoint of device where grub should install its files to
559     @device: partition where grub should be installed to"""
560
561     if options.dryrun:
562         logging.info("Would execute grub-install [--root-directory=mount_point] %s now.", device)
563     else:
564         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
565         register_tmpfile(device_mountpoint)
566         try:
567             try:
568                 mount(device, device_mountpoint, "")
569
570                 # If using --grub-mbr then make sure we install grub in MBR instead of PBR
571                 if options.grubmbr:
572                     logging.debug("Using option --grub-mbr ...")
573                     if device[-1:].isdigit():
574                         grub_device = re.match(r'(.*?)\d*$', device).group(1)
575                     else:
576                         grub_device = device
577                 else:
578                     grub_device = device
579
580                 logging.info("Installing grub as bootloader")
581                 for opt in ["", "--force" ]:
582                     logging.debug("grub-install --recheck %s --no-floppy --root-directory=%s %s",
583                                   opt, device_mountpoint, grub_device)
584                     proc = subprocess.Popen(["grub-install", "--recheck", opt, "--no-floppy",
585                                              "--root-directory=%s" % device_mountpoint, grub_device],
586                                             stdout=file(os.devnull, "r+"))
587                     proc.wait()
588                     if proc.returncode == 0:
589                         break
590
591                 if proc.returncode != 0:
592                     # raise Exception("error executing grub-install")
593                     logging.critical("Fatal: error executing grub-install "
594                                      + "(please check the grml2usb FAQ or drop the --grub option)")
595                     logging.critical("Note:  if using grub2 consider using "
596                                      + "the --grub-mbr option as grub considers PBR problematic.")
597                     cleanup()
598                     sys.exit(1)
599             except CriticalException, error:
600                 logging.critical("Fatal: %s", error)
601                 cleanup()
602                 sys.exit(1)
603
604         finally:
605             unmount(device_mountpoint, "")
606             os.rmdir(device_mountpoint)
607             unregister_tmpfile(device_mountpoint)
608
609
610 def install_syslinux(device):
611     """Install syslinux on specified device.
612
613     @device: partition where syslinux should be installed to"""
614
615     if options.dryrun:
616         logging.info("Would install syslinux as bootloader on %s", device)
617         return 0
618
619     # syslinux -d boot/isolinux /dev/sdb1
620     logging.info("Installing syslinux as bootloader")
621     logging.debug("syslinux -d boot/syslinux %s", device)
622     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
623     proc.wait()
624     if proc.returncode != 0:
625         raise CriticalException("Error executing syslinux (either try --fat16 or use grub?)")
626
627
628 def install_bootloader(device):
629     """Install bootloader on specified device.
630
631     @device: partition where bootloader should be installed to"""
632
633     # by default we use grub, so install syslinux only on request
634     if options.grub:
635         try:
636             install_grub(device)
637         except CriticalException, error:
638             logging.critical("Fatal: %s", error)
639             cleanup()
640             sys.exit(1)
641     else:
642         try:
643             install_syslinux(device)
644         except CriticalException, error:
645             logging.critical("Fatal: %s", error)
646             cleanup()
647             sys.exit(1)
648
649
650 def install_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
651     """install 'mbr' master boot record (MBR) on a device
652
653     Retrieve the partition table from "device", install an MBR from the
654     "mbrtemplate" file, set the "partition" (0..3) active, and install the
655     result back to "device".
656
657     @mbrtemplate: default MBR file
658
659     @device: name of a file assumed to be a hard disc (or USB stick) image, or
660     something like "/dev/sdb"
661
662     @partition: must be a number between 0 and 3, inclusive
663
664     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
665     ismirbsdmbr) bytes.
666
667     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
668     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
669     false then leave the mirbsdmbr specific flag set to FFh, set all
670     active flags to 0 and set the active flag of the partition to 80h.  Note:
671     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
672     the active flag."""
673
674     logging.info("Installing default MBR")
675
676     if not os.path.isfile(mbrtemplate):
677         logging.critical("Error: %s can not be read.", mbrtemplate)
678         raise CriticalException("Error installing MBR (either try --syslinux-mbr or install missing file \"%s\"?)" % mbrtemplate)
679
680     if partition is not None and ((partition < 0) or (partition > 3)):
681         logging.warn("Cannot activate partition %d" % partition)
682         partition = None
683
684     if ismirbsdmbr:
685         nmbrbytes = 439
686     else:
687         nmbrbytes = 440
688
689     tmpf = tempfile.NamedTemporaryFile()
690
691     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
692     proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
693                             stderr=file(os.devnull, "r+"))
694     proc.wait()
695     if proc.returncode != 0:
696         raise Exception("error executing dd (first run)")
697
698     logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc", mbrtemplate,
699                   tmpf.name, nmbrbytes)
700     proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes,
701                              "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
702     proc.wait()
703     if proc.returncode != 0:
704         raise Exception("error executing dd (second run)")
705
706     mbrcode = tmpf.file.read(512)
707     if len(mbrcode) < 512:
708         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
709
710     if partition is not None:
711         if ismirbsdmbr:
712             mbrcode = mbrcode[0:439] + chr(partition) + \
713                     mbrcode[440:510] + "\x55\xAA"
714         else:
715             actives = ["\x00", "\x00", "\x00", "\x00"]
716             actives[partition] = "\x80"
717             mbrcode = mbrcode[0:446] + actives[0] + \
718                     mbrcode[447:462] + actives[1] + \
719                     mbrcode[463:478] + actives[2] + \
720                     mbrcode[479:494] + actives[3] + \
721                     mbrcode[495:510] + "\x55\xAA"
722     
723     tmpf.file.seek(0)
724     tmpf.file.truncate()
725     tmpf.file.write(mbrcode)
726     tmpf.file.close()
727
728     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device)
729     proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % device, "bs=512", "count=1",
730                              "conv=notrunc"], stderr=file(os.devnull, "r+"))
731     proc.wait()
732     if proc.returncode != 0:
733         raise Exception("error executing dd (third run)")
734     del tmpf
735
736
737 def is_writeable(device):
738     """Check if the device is writeable for the current user
739
740     @device: partition where bootloader should be installed to"""
741
742     if not device:
743         return False
744         #raise Exception("no device for checking write permissions")
745
746     if not os.path.exists(device):
747         return False
748
749     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
750
751
752 def mount(source, target, mount_options):
753     """Mount specified source on given target
754
755     @source: name of device/ISO that should be mounted
756     @target: directory where the ISO should be mounted to
757     @options: mount specific options"""
758
759     # note: options.dryrun does not work here, as we have to
760     # locate files and identify the grml flavour
761
762     for x in file('/proc/mounts').readlines():
763         if x.startswith(source):
764             raise CriticalException("Error executing mount: %s already mounted - " % source
765                                     + "please unmount before invoking grml2usb")
766
767     if os.path.isdir(source):
768         logging.debug("Source %s is not a device, therefore not mounting.", source)
769         return 0
770
771     logging.debug("mount %s %s %s", mount_options, source, target)
772     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
773     proc.wait()
774     if proc.returncode != 0:
775         raise CriticalException("Error executing mount (no filesystem on the partition?)")
776     else:
777         logging.debug("register_mountpoint(%s)", target)
778         register_mountpoint(target)
779
780
781 def unmount(target, unmount_options):
782     """Unmount specified target
783
784     @target: target where something is mounted on and which should be unmounted
785     @options: options for umount command"""
786
787     # make sure we unmount only already mounted targets
788     target_unmount = False
789     mounts = open('/proc/mounts').readlines()
790     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
791     for line in mounts:
792         if re.match(mountstring, line):
793             target_unmount = True
794
795     if not target_unmount:
796         logging.debug("%s not mounted anymore", target)
797     else:
798         logging.debug("umount %s %s", list(unmount_options), target)
799         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
800         proc.wait()
801         if proc.returncode != 0:
802             raise Exception("Error executing umount")
803         else:
804             logging.debug("unregister_mountpoint(%s)", target)
805             unregister_mountpoint(target)
806
807
808 def check_for_usbdevice(device):
809     """Check whether the specified device is a removable USB device
810
811     @device: device name, like /dev/sda1 or /dev/sda
812     """
813
814     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
815     # newer systems:
816     usbdev = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
817     if not os.path.isfile(usbdev):
818         # Ubuntu with kernel 2.6.24 for example:
819         usbdev = os.path.realpath('/sys/block/' + usbdevice + '/removable')
820
821     if os.path.isfile(usbdev):
822         is_usb = open(usbdev).readline()
823         if is_usb.find("1"):
824             return 0
825
826     return 1
827
828
829 def check_for_fat(partition):
830     """Check whether specified partition is a valid VFAT/FAT16 filesystem
831
832     @partition: device name of partition"""
833
834     if not os.access(partition, os.R_OK):
835         raise CriticalException("Failed to read device %s"
836                 " (wrong UID/permissions or device/directory not present?)" % partition)
837
838     try:
839         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
840                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
841         filesystem = udev_info.communicate()[0].rstrip()
842
843         if filesystem != "vfat":
844             raise CriticalException(
845                     "Partition %s does not contain a FAT16 filesystem. "
846                     "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
847
848     except OSError:
849         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
850
851
852 def mkdir(directory):
853     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
854
855     # just silently pass as it's just fine it the directory exists
856     if not os.path.isdir(directory):
857         try:
858             os.makedirs(directory)
859         # pylint: disable-msg=W0704
860         except OSError:
861             pass
862
863
864 def exec_rsync(source, target):
865     """Simple wrapper around rsync to install files
866
867     @source: source file/directory
868     @target: target file/directory"""
869     logging.debug("Source: %s / Target: %s", source, target)
870     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
871     proc.wait()
872     if proc.returncode == 12:
873         logging.critical("Fatal: No space left on device")
874         cleanup()
875         sys.exit(1)
876
877     if proc.returncode != 0:
878         logging.critical("Fatal: could not install %s", source)
879         cleanup()
880         sys.exit(1)
881
882
883 def write_uuid(target_file):
884     """Generates an returns uuid and write it to the specified file
885
886     @target_file: filename to write the uuid to
887     """
888
889     fileh = open(target_file, 'w')
890     uid = str(uuid.uuid4())
891     fileh.write(uid)
892     fileh.close()
893     return uid
894
895
896 def get_uuid(target):
897     """Get the uuid of the specified target. Will generate an uuid if none exist.
898
899     @target: directory/mountpoint containing the grml layout
900     """
901
902     conf_target = target + "/conf/"
903     uuid_file_name = conf_target + "/bootid.txt"
904     if os.path.isdir(conf_target):
905         if os.path.isfile(uuid_file_name):
906             uuid_file = open(uuid_file_name, 'r')
907             uid = uuid_file.readline().strip()
908             uuid_file.close()
909             return uid
910         else:
911             return write_uuid(uuid_file_name)
912     else:
913         execute(mkdir, conf_target)
914         return write_uuid(uuid_file_name)
915
916
917 def copy_system_files(grml_flavour, iso_mount, target):
918     """copy grml's main files (like squashfs, kernel and initrd) to a given target
919
920     @grml_flavour: name of grml flavour the configuration should be generated for
921     @iso_mount: path where a grml ISO is mounted on
922     @target: path where grml's main files should be copied to"""
923
924     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
925     if squashfs is None:
926         logging.critical("Fatal: squashfs file not found"
927         ", please check that your iso is not corrupt")
928         raise CriticalException("error locating squashfs file")
929     else:
930         squashfs_target = target + '/live/' + grml_flavour + '/'
931         execute(mkdir, squashfs_target)
932     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
933
934     for prefix in grml_flavour + "/", "":
935         filesystem_module = search_file(prefix + 'filesystem.module', iso_mount)
936         if filesystem_module:
937             break
938     if filesystem_module is None:
939         logging.critical("Fatal: filesystem.module not found")
940         raise CriticalException("error locating filesystem.module file")
941     else:
942         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
943
944
945     release_path = 'boot/release/' + grml_flavour.replace('-', '')
946     release_target = target + "/" + release_path
947     execute(mkdir, release_target)
948
949     prefix = ""
950     if os.path.isdir(iso_mount + '/boot/release'):
951         prefix = release_path + '/'
952
953     kernel = search_file(prefix + 'linux26', iso_mount)
954     if kernel is None:
955         logging.critical("Fatal kernel not found")
956         raise CriticalException("error locating kernel file")
957     else:
958         exec_rsync(kernel, release_target + '/linux26')
959
960     initrd = search_file(prefix + 'initrd.gz', iso_mount)
961     if initrd is None:
962         logging.critical("Fatal: initrd not found")
963         raise CriticalException("error locating initrd file")
964     else:
965         exec_rsync(initrd, release_target + '/initrd.gz')
966
967
968 def update_grml_versions(iso_mount, target):
969     """Update the grml version file on a cd
970     Returns true if version was updated successfully,
971     False if grml-version does not exist yet on the mountpoint
972
973     @iso_mount: string of the iso mount point
974     @target: path of the target mount point
975     """
976     grml_target = target + '/grml/'
977     target_grml_version_file = search_file('grml-version', grml_target)
978     if target_grml_version_file:
979         iso_grml_version_file = search_file('grml-version', iso_mount)
980         if not iso_grml_version_file:
981             logging.warn("Warning: %s could not be found - can not install it", iso_grml_version_file)
982             return False
983         try:
984             # read the flavours from the iso image
985             iso_versions = {}
986             iso_file = open(iso_grml_version_file, 'r')
987             for line in iso_file:
988                 iso_versions[get_flavour(line)] = line.strip()
989
990             # update the existing flavours on the target
991             for line in fileinput.input([target_grml_version_file], inplace=1):
992                 flavour = get_flavour(line)
993                 if flavour in iso_versions.keys():
994                     print iso_versions.pop(flavour)
995                 else:
996                     print line.strip()
997             fileinput.close()
998
999             target_file = open(target_grml_version_file, 'a')
1000             # add the new flavours from the current iso
1001             for flavour in iso_versions:
1002                 target_file.write("%s\n" % iso_versions[flavour])
1003         except IOError:
1004             logging.warn("Warning: Could not write file")
1005         finally:
1006             iso_file.close()
1007             target_file.close()
1008         return True
1009     else:
1010         return False
1011
1012 def copy_grml_files(iso_mount, target):
1013     """copy some minor grml files to a given target
1014
1015     @iso_mount: path where a grml ISO is mounted on
1016     @target: path where grml's main files should be copied to"""
1017
1018     grml_target = target + '/grml/'
1019     execute(mkdir, grml_target)
1020
1021     copy_files = [ 'grml-cheatcodes.txt', 'LICENSE.txt', 'md5sums', 'README.txt' ]
1022     # handle grml-version
1023     if not update_grml_versions(iso_mount, target):
1024         copy_files.append('grml-version')
1025
1026     for myfile in copy_files:
1027         grml_file = search_file(myfile, iso_mount)
1028         if grml_file is None:
1029             logging.warn("Warning: file %s could not be found - can not install it", myfile)
1030         else:
1031             exec_rsync(grml_file, grml_target + myfile)
1032
1033     grml_web_target = grml_target + '/web/'
1034     execute(mkdir, grml_web_target)
1035
1036     for myfile in 'index.html', 'style.css':
1037         grml_file = search_file(myfile, iso_mount)
1038         if grml_file is None:
1039             logging.warn("Warning: file %s could not be found - can not install it", myfile)
1040         else:
1041             exec_rsync(grml_file, grml_web_target + myfile)
1042
1043     grml_webimg_target = grml_web_target + '/images/'
1044     execute(mkdir, grml_webimg_target)
1045
1046     for myfile in 'button.png', 'favicon.png', 'linux.jpg', 'logo.png':
1047         grml_file = search_file(myfile, iso_mount)
1048         if grml_file is None:
1049             logging.warn("Warning: file %s could not be found - can not install it", myfile)
1050         else:
1051             exec_rsync(grml_file, grml_webimg_target + myfile)
1052
1053
1054 def handle_addon_copy(filename, dst, iso_mount, ignore_errors=False):
1055     """handle copy of optional addons
1056
1057     @filename: filename of the addon
1058     @dst: destination directory
1059     @iso_mount: location of the iso mount
1060     @ignore_errors: don't report missing files
1061     """
1062     file_location = search_file(filename, iso_mount)
1063     if file_location is None:
1064         if not ignore_errors:
1065             logging.warn("Warning: %s not found (that's fine if you don't need it)",  filename)
1066     else:
1067         exec_rsync(file_location, dst)
1068
1069
1070 def copy_addons(iso_mount, target):
1071     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1072
1073     @iso_mount: path where a grml ISO is mounted on
1074     @target: path where grml's main files should be copied to"""
1075
1076     addons = target + '/boot/addons/'
1077     execute(mkdir, addons)
1078
1079     # grub all-in-one image
1080     handle_addon_copy('allinone.img', addons, iso_mount)
1081
1082     # bsd imag
1083     handle_addon_copy('bsd4grml', addons, iso_mount)
1084
1085     handle_addon_copy('balder10.imz', addons, iso_mount)
1086
1087     # install hdt and pci.ids only when using syslinux (grub doesn't support it)
1088     if options.syslinux:
1089         # hdt (hardware detection tool) image
1090         hdtimg = search_file('hdt.c32', iso_mount)
1091         if hdtimg:
1092             exec_rsync(hdtimg, addons + '/hdt.c32')
1093
1094         # pci.ids file
1095         picids = search_file('pci.ids', iso_mount)
1096         if picids:
1097             exec_rsync(picids, addons + '/pci.ids')
1098
1099     # memdisk image
1100     handle_addon_copy('memdisk', addons, iso_mount)
1101
1102     # memtest86+ image
1103     handle_addon_copy('memtest', addons, iso_mount)
1104
1105     # gpxe.lkrn: got replaced by ipxe
1106     handle_addon_copy('gpxe.lkrn', addons, iso_mount, ignore_errors=True)
1107
1108     # ipxe.lkrn
1109     handle_addon_copy('ipxe.lkrn', addons, iso_mount)
1110
1111 def glob_and_copy(filepattern, dst):
1112     """Glob on specified filepattern and copy the result to dst
1113
1114     @filepattern: globbing pattern
1115     @dst: target directory
1116     """
1117     for name in glob.glob(filepattern):
1118         copy_if_exist(name, dst)
1119
1120 def search_and_copy(filename, search_path, dst):
1121     """Search for the specified filename at searchpath and copy it to dst
1122
1123     @filename: filename to look for
1124     @search_path: base search file
1125     @dst: destionation to copy the file to
1126     """
1127     file_location = search_file(filename, search_path)
1128     copy_if_exist(file_location, dst)
1129
1130 def copy_if_exist(filename, dst):
1131     """Copy filename to dst if filename is set.
1132
1133     @filename: a filename
1134     @dst: dst file
1135     """
1136     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1137         exec_rsync(filename, dst)
1138
1139 def copy_bootloader_files(iso_mount, target, grml_flavour):
1140     """Copy grml's bootloader files to a given target
1141
1142     @iso_mount: path where a grml ISO is mounted on
1143     @target: path where grml's main files should be copied to
1144     @grml_flavour: name of the current processed grml_flavour
1145     """
1146
1147     syslinux_target = target + '/boot/syslinux/'
1148     execute(mkdir, syslinux_target)
1149
1150     grub_target = target + '/boot/grub/'
1151     execute(mkdir, grub_target)
1152
1153     logo = search_file('logo.16', iso_mount)
1154     exec_rsync(logo, syslinux_target + 'logo.16')
1155
1156     bootx64_efi = search_file('bootx64.efi', iso_mount)
1157     if bootx64_efi:
1158         mkdir(target + '/efi/boot/')
1159         exec_rsync(bootx64_efi, target + '/efi/boot/bootx64.efi')
1160
1161     efi_img = search_file('efi.img', iso_mount)
1162     if efi_img:
1163         mkdir(target + '/boot/')
1164         exec_rsync(efi_img, target + '/boot/efi.img')
1165
1166     for ffile in ['f%d' % number for number in range(1, 11) ]:
1167         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1168
1169     loopback_cfg = search_file("loopback.cfg", iso_mount)
1170     if loopback_cfg:
1171         directory = os.path.dirname(loopback_cfg)
1172         directory = directory.replace(iso_mount, "")
1173         mkdir(os.path.join(target, directory))
1174         exec_rsync(loopback_cfg, target + os.path.sep + directory)
1175
1176     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1177     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1178     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1179         os.unlink(syslinux_target + 'ldlinux.sys')
1180
1181     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1182     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1183
1184     if not source_dir:
1185         logging.critical("Fatal: file default.cfg could not be found.")
1186         logging.critical("Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...")
1187         logging.critical("       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1188         raise
1189
1190     for expr in name, 'distri.cfg', \
1191         defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1192         'isoprompt.cfg', 'options.cfg', \
1193         'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1194         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1195
1196     for filename in glob.glob1(syslinux_target, "*.c32"):
1197         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1198
1199
1200     # copy the addons_*.cfg file to the new syslinux directory
1201     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1202
1203     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1204
1205
1206     grub_target = target + '/boot/grub/'
1207     execute(mkdir, grub_target)
1208
1209     if not os.path.isfile(GRML2USB_BASE + "/grub/splash.xpm.gz"):
1210         logging.critical("Error: %s/grub/splash.xpm.gz can not be read.", GRML2USB_BASE)
1211         logging.critical("Please make sure you've installed the grml2usb (Debian) package!")
1212         raise
1213     else:
1214         exec_rsync(GRML2USB_BASE + '/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz')
1215
1216     # grml splash in grub
1217     copy_if_exist(GRML2USB_BASE + "/grub/grml.png", grub_target + 'grml.png')
1218
1219     # font file for graphical bootsplash in grub
1220     copy_if_exist('/usr/share/grub/ascii.pf2', grub_target + 'ascii.pf2')
1221
1222     # always copy grub content as it might be useful
1223
1224     glob_and_copy(iso_mount + '/boot/grub/*.mod', grub_target)
1225     glob_and_copy(iso_mount + '/boot/grub/*.img', grub_target)
1226     glob_and_copy(iso_mount + '/boot/grub/stage*', grub_target)
1227
1228 def install_iso_files(grml_flavour, iso_mount, device, target):
1229     """Copy files from ISO to given target
1230
1231     @grml_flavour: name of grml flavour the configuration should be generated for
1232     @iso_mount: path where a grml ISO is mounted on
1233     @device: device/partition where bootloader should be installed to
1234     @target: path where grml's main files should be copied to"""
1235
1236     global GRML_DEFAULT
1237     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1238     if options.dryrun:
1239         return 0
1240     elif not options.bootloaderonly:
1241         logging.info("Copying files. This might take a while....")
1242         try:
1243             copy_system_files(grml_flavour, iso_mount, target)
1244             copy_grml_files(iso_mount, target)
1245         except CriticalException, error:
1246             logging.critical("Execution failed: %s", error)
1247             sys.exit(1)
1248
1249     if not options.skipaddons:
1250         if not search_file('addons', iso_mount):
1251             logging.info("Could not find addons, therefore not installing.")
1252         else:
1253             copy_addons(iso_mount, target)
1254
1255     if not options.copyonly:
1256         copy_bootloader_files(iso_mount, target, grml_flavour)
1257
1258         if not options.dryrun:
1259             handle_bootloader_config(grml_flavour, device, target)
1260
1261     # make sure we sync filesystems before returning
1262     proc = subprocess.Popen(["sync"])
1263     proc.wait()
1264
1265
1266 def get_flavour(flavour_str):
1267     """Returns the flavour of a grml version string
1268     """
1269     return re.match(r'[\w-]*', flavour_str).group()
1270
1271 def identify_grml_flavour(mountpath):
1272     """Get name of grml flavour
1273
1274     @mountpath: path where the grml ISO is mounted to
1275     @return: name of grml-flavour"""
1276
1277     version_file = search_file('grml-version', mountpath)
1278
1279     if version_file == "":
1280         logging.critical("Error: could not find grml-version file.")
1281         raise
1282
1283     flavours = []
1284     tmpfile = None
1285     try:
1286         tmpfile = open(version_file, 'r')
1287         for line in tmpfile.readlines():
1288             flavours.append(get_flavour(line))
1289     except TypeError, e:
1290         raise
1291     except Exception, e:
1292         logging.critical("Unexpected error: %s", e)
1293         raise
1294     finally:
1295         if tmpfile:
1296             tmpfile.close()
1297
1298     return flavours
1299
1300
1301 def modify_grub_config(filename):
1302     """Adjust bootoptions for a grub file
1303
1304     @filename: filename to modify
1305     """
1306     if options.removeoption:
1307         regexe = []
1308         for regex in options.removeoption:
1309             regexe.append(re.compile(r'%s' % regex))
1310
1311         option_re = re.compile(r'(.*/boot/release/.*linux26.*)')
1312
1313         for line in fileinput.input(filename, inplace=1):
1314             if regexe and option_re.search(line):
1315                 for regex in regexe:
1316                     line = regex.sub(' ', line)
1317
1318             sys.stdout.write(line)
1319
1320         fileinput.close()
1321
1322 def handle_grub2_config(grml_flavour, grub_target, bootopt):
1323     """Main handler for generating grub2 configuration
1324
1325     @grml_flavour: name of grml flavour the configuration should be generated for
1326     @grub_target: path of grub's configuration files
1327     @bootoptions: additional bootoptions that should be used by default"""
1328
1329     # grub2 config
1330     grub2_cfg = grub_target + 'grub.cfg'
1331     logging.debug("Creating grub2 configuration file (grub.cfg)")
1332
1333     global GRML_DEFAULT
1334
1335     # install main configuration only *once*, no matter how many ISOs we have:
1336     grub_flavour_is_default = False
1337     if os.path.isfile(grub2_cfg):
1338         string = open(grub2_cfg).readline()
1339         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1340         if not re.match(main_identifier, string):
1341             grub2_config_file = open(grub2_cfg, 'w')
1342             GRML_DEFAULT = grml_flavour
1343             grub_flavour_is_default = True
1344             grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1345             grub2_config_file.close()
1346     else:
1347         grub2_config_file = open(grub2_cfg, 'w')
1348         GRML_DEFAULT = grml_flavour
1349         grub_flavour_is_default = True
1350         grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1351         grub2_config_file.close()
1352
1353     # install flavour specific configuration only *once* as well
1354     grub_flavour_config = True
1355     if os.path.isfile(grub2_cfg):
1356         string = open(grub2_cfg).readlines()
1357         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1358         for line in string:
1359             if flavour.match(line):
1360                 grub_flavour_config = False
1361
1362     if grub_flavour_config:
1363         grub2_config_file = open(grub2_cfg, 'a')
1364         # display only if the grml flavour isn't the default
1365         if not grub_flavour_is_default:
1366             GRML_FLAVOURS.add(grml_flavour)
1367         grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, bootopt))
1368         grub2_config_file.close()
1369
1370     modify_grub_config(grub2_cfg)
1371
1372
1373 def get_bootoptions(grml_flavour):
1374     """Returns bootoptions for specific flavour
1375
1376     @grml_flavour: name of the grml_flavour
1377     """
1378     # do NOT write "None" in kernel cmdline
1379     if not options.bootoptions:
1380         bootopt = ""
1381     else:
1382         bootopt = " ".join(options.bootoptions)
1383     bootopt = bootopt.replace("%flavour", grml_flavour)
1384     return bootopt
1385
1386
1387 def handle_grub_config(grml_flavour, device, target):
1388     """Main handler for generating grub (v1 and v2) configuration
1389
1390     @grml_flavour: name of grml flavour the configuration should be generated for
1391     @device: device/partition where grub should be installed to
1392     @target: path of grub's configuration files"""
1393
1394     logging.debug("Generating grub configuration")
1395
1396     grub_target = target + '/boot/grub/'
1397
1398     bootopt = get_bootoptions(grml_flavour)
1399
1400     # write grub.cfg
1401     handle_grub2_config(grml_flavour, grub_target, bootopt)
1402
1403
1404 def initial_syslinux_config(target):
1405     """Generates intial syslinux configuration
1406
1407     @target path of syslinux's configuration files"""
1408
1409     target = target + "/"
1410     filename = target + "grmlmain.cfg"
1411     if os.path.isfile(target + "grmlmain.cfg"):
1412         return
1413     data = open(filename, "w")
1414     data.write(generate_main_syslinux_config())
1415     data.close()
1416
1417     filename = target + "hiddens.cfg"
1418     data = open(filename, "w")
1419     data.write("include hidden.cfg\n")
1420     data.close()
1421
1422 def add_entry_if_not_present(filename, entry):
1423     """Write entry into filename if entry is not already in the file
1424
1425     @filanme: name of the file
1426     @entry: data to write to the file
1427     """
1428     data = open(filename, "a+")
1429     for line in data:
1430         if line == entry:
1431             break
1432     else:
1433         data.write(entry)
1434
1435     data.close()
1436
1437 def get_flavour_filename(flavour):
1438     """Generate a iso9960 save filename out of the specified flavour
1439
1440     @flavour: grml flavour
1441     """
1442     return flavour.replace('-', '_')
1443
1444 def adjust_syslinux_bootoptions(src, flavour):
1445     """Adjust existing bootoptions of specified syslinux config to
1446     grml2usb specific ones, e.g. change the location of the kernel...
1447
1448     @src: config file to alter
1449     @flavour: grml flavour
1450     """
1451
1452     append_re = re.compile("^(\s*append.*/boot/release.*)$", re.I)
1453     boot_re = re.compile("/boot/([a-zA-Z0-9_]+/)+([a-zA-Z0-9._]+)")
1454     # flavour_re = re.compile("(label.*)(grml\w+)")
1455     default_re = re.compile("(default.cfg)")
1456     bootid_re = re.compile("bootid=[\w_-]+")
1457     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1458
1459     bootopt = get_bootoptions(flavour)
1460
1461     regexe = []
1462     option_re = None
1463     if options.removeoption:
1464         option_re = re.compile(r'/boot/release/.*/initrd.gz')
1465
1466         for regex in options.removeoption:
1467             regexe.append(re.compile(r'%s' % regex))
1468
1469     for line in fileinput.input(src, inplace=1):
1470         line = boot_re.sub(r'/boot/release/%s/\2 ' % flavour.replace('-', ''), line)
1471         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1472         line = default_re.sub(r'%s-\1' % flavour, line)
1473         line = bootid_re.sub('', line)
1474         line = live_media_path_re.sub('', line)
1475         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1476         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1477         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1478         if option_re and option_re.search(line):
1479             for regex in regexe:
1480                 line = regex.sub(' ', line)
1481         sys.stdout.write(line)
1482     fileinput.close()
1483
1484 def adjust_labels(src, replacement):
1485     """Adjust the specified labels in the syslinux config file src with
1486     specified replacement
1487     """
1488     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1489     for line in fileinput.input(src, inplace=1):
1490         line = label_re.sub(replacement, line)
1491         sys.stdout.write(line)
1492     fileinput.close()
1493
1494
1495 def add_syslinux_entry(filename, grml_flavour):
1496     """Add includes for a specific grml_flavour to the specified filename
1497
1498     @filename: syslinux config file
1499     @grml_flavour: grml flavour to add
1500     """
1501
1502     entry_filename = "option_%s.cfg" % grml_flavour
1503     entry = "include %s\n" % entry_filename
1504
1505     add_entry_if_not_present(filename, entry)
1506     path = os.path.dirname(filename)
1507
1508     data = open(path + "/" + entry_filename, "w")
1509     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1510     data.close()
1511
1512 def modify_filenames(grml_flavour, target, filenames):
1513     """Replace the standarf filenames with the new ones
1514
1515     @grml_flavour: grml-flavour strin
1516     @target: directory where the files are located
1517     @filenames: list of filenames to alter
1518     """
1519     grml_filename = grml_flavour.replace('-', '_')
1520     for filename in filenames:
1521         old_filename = "%s/%s" % (target, filename)
1522         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1523         os.rename(old_filename, new_filename)
1524
1525
1526 def remove_default_entry(filename):
1527     """Remove the default entry from specified syslinux file
1528
1529     @filename: syslinux config file
1530     """
1531     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1532     for line in fileinput.input(filename, inplace=1):
1533         if default_re.match(line):
1534             continue
1535         sys.stdout.write(line)
1536     fileinput.close()
1537
1538
1539 def handle_syslinux_config(grml_flavour, target):
1540     """Main handler for generating syslinux configuration
1541
1542     @grml_flavour: name of grml flavour the configuration should be generated for
1543     @target: path of syslinux's configuration files"""
1544
1545     logging.debug("Generating syslinux configuration")
1546     syslinux_target = target + '/boot/syslinux/'
1547     # should be present via  copy_bootloader_files(), but make sure it exits:
1548     execute(mkdir, syslinux_target)
1549     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1550
1551
1552     # install main configuration only *once*, no matter how many ISOs we have:
1553     syslinux_config_file = open(syslinux_cfg, 'w')
1554     syslinux_config_file.write("TIMEOUT 300\n")
1555     syslinux_config_file.write("include vesamenu.cfg\n")
1556     syslinux_config_file.close()
1557
1558     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1559     prompt_name.write('menu label S^yslinux prompt\n')
1560     prompt_name.close()
1561
1562     initial_syslinux_config(syslinux_target)
1563     flavour_filename = grml_flavour.replace('-', '_')
1564
1565     if search_file('default.cfg', syslinux_target):
1566         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1567
1568     filename = search_file("new_hidden.cfg", syslinux_target)
1569
1570
1571     # process hidden file
1572     if not search_file("hidden.cfg", syslinux_target):
1573         new_hidden = syslinux_target + "hidden.cfg"
1574         os.rename(filename, new_hidden)
1575         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1576     else:
1577         new_hidden_file =  "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1578         os.rename(filename, new_hidden_file)
1579         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1580         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1581         entry = 'include %s_hidden.cfg\n' % flavour_filename
1582         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1583
1584
1585
1586     new_default = "%s_default.cfg" % (flavour_filename)
1587     entry = 'include %s\n' % new_default
1588     defaults_file = '%s/defaults.cfg' % syslinux_target
1589     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1590     new_grml_cfg = "%s/%s_grml.cfg" % ( syslinux_target, flavour_filename)
1591
1592     if os.path.isfile(defaults_file):
1593
1594         # remove default menu entry in menu
1595         remove_default_entry(new_default_with_path)
1596
1597         # adjust all labels for additional isos
1598         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1599         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1600
1601     # always adjust bootoptions
1602     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1603     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1604
1605     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1606
1607     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1608
1609
1610 def handle_bootloader_config(grml_flavour, device, target):
1611     """Main handler for generating bootloader's configuration
1612
1613     @grml_flavour: name of grml flavour the configuration should be generated for
1614     @device: device/partition where bootloader should be installed to
1615     @target: path of bootloader's configuration files"""
1616
1617     global UUID
1618     UUID = get_uuid(target)
1619     if options.skipsyslinuxconfig:
1620         logging.info("Skipping generation of syslinux configuration as requested.")
1621     else:
1622         try:
1623             handle_syslinux_config(grml_flavour, target)
1624         except CriticalException, error:
1625             logging.critical("Fatal: %s", error)
1626             sys.exit(1)
1627
1628     if options.skipgrubconfig:
1629         logging.info("Skipping generation of grub configuration as requested.")
1630     else:
1631         try:
1632             handle_grub_config(grml_flavour, device, target)
1633         except CriticalException, error:
1634             logging.critical("Fatal: %s", error)
1635             sys.exit(1)
1636
1637
1638
1639 def install(image, device):
1640     """Install a grml image to the specified device
1641
1642     @image: directory or is file
1643     @device: partition or directory to install the device
1644     """
1645     iso_mountpoint = image
1646     remove_image_mountpoint = False
1647     if os.path.isdir(image):
1648         logging.info("Using %s as install base", image)
1649     else:
1650         logging.info("Using ISO %s", image)
1651         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1652         register_tmpfile(iso_mountpoint)
1653         remove_image_mountpoint = True
1654         try:
1655             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1656         except CriticalException, error:
1657             logging.critical("Fatal: %s", error)
1658             sys.exit(1)
1659
1660     try:
1661         install_grml(iso_mountpoint, device)
1662     finally:
1663         if remove_image_mountpoint:
1664             try:
1665                 remove_mountpoint(iso_mountpoint)
1666             except CriticalException, error:
1667                 logging.critical("Fatal: %s", error)
1668                 cleanup()
1669
1670
1671
1672 def install_grml(mountpoint, device):
1673     """Main logic for copying files of the currently running grml system.
1674
1675     @mountpoin: directory where currently running live system resides (usually /live/image)
1676     @device: partition where the specified ISO should be installed to"""
1677
1678     device_mountpoint = device
1679     if os.path.isdir(device):
1680         logging.info("Specified device is not a directory, therefore not mounting.")
1681         remove_device_mountpoint = False
1682     else:
1683         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1684         register_tmpfile(device_mountpoint)
1685         remove_device_mountpoint = True
1686         try:
1687             check_for_fat(device)
1688             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1689         except CriticalException, error:
1690             try:
1691                 mount(device, device_mountpoint, "")
1692             except CriticalException, error:
1693                 logging.critical("Fatal: %s", error)
1694                 raise
1695     try:
1696         grml_flavours = identify_grml_flavour(mountpoint)
1697         for flavour in set(grml_flavours):
1698             if not flavour:
1699                 logging.warning("No valid flavour found, please check your iso")
1700             logging.info("Identified grml flavour \"%s\".", flavour)
1701             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1702             GRML_FLAVOURS.add(flavour)
1703     finally:
1704         if remove_device_mountpoint:
1705             remove_mountpoint(device_mountpoint)
1706
1707 def remove_mountpoint(mountpoint):
1708     """remove a registred mountpoint
1709     """
1710
1711     try:
1712         unmount(mountpoint, "")
1713         if os.path.isdir(mountpoint):
1714             os.rmdir(mountpoint)
1715             unregister_tmpfile(mountpoint)
1716     except CriticalException, error:
1717         logging.critical("Fatal: %s", error)
1718         cleanup()
1719
1720 def handle_mbr(device):
1721     """Main handler for installing master boot record (MBR)
1722
1723     @device: device where the MBR should be installed to"""
1724
1725     if options.dryrun:
1726         logging.info("Would install MBR")
1727         return 0
1728
1729     if device[-1:].isdigit():
1730         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1731         partition_number = int(device[-1:]) - 1
1732     else:
1733         logging.warn("Could not detect partition number, not activating partition")
1734         partition_number = None
1735
1736     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1737     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1738     if mbr_device == "/dev/loop":
1739         mbr_device = device
1740         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1741
1742     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1743     if options.syslinuxmbr:
1744         mbrcode = '/usr/lib/syslinux/mbr.bin'
1745     elif options.mbrmenu:
1746         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1747
1748     try:
1749         install_mbr(mbrcode, mbr_device, partition_number, True)
1750     except IOError, error:
1751         logging.critical("Execution failed: %s", error)
1752         sys.exit(1)
1753     except Exception, error:
1754         logging.critical("Execution failed: %s", error)
1755         sys.exit(1)
1756
1757
1758 def handle_vfat(device):
1759     """Check for FAT specific settings and options
1760
1761     @device: device that should checked / formated"""
1762
1763     # make sure we have mkfs.vfat available
1764     if options.fat16:
1765         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1766             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1767             logging.critical('Please make sure to install dosfstools.')
1768             sys.exit(1)
1769
1770         if options.force:
1771             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1772         else:
1773             # make sure the user is aware of what he is doing
1774             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1775             if f == "y" or f == "Y":
1776                 logging.info("Note: you can skip this question using the option --force")
1777             else:
1778                 sys.exit(1)
1779         try:
1780             mkfs_fat16(device)
1781         except CriticalException, error:
1782             logging.critical("Execution failed: %s", error)
1783             sys.exit(1)
1784
1785     # check for vfat filesystem
1786     if device is not None and not os.path.isdir(device) and options.syslinux:
1787         try:
1788             check_for_fat(device)
1789         except CriticalException, error:
1790             logging.critical("Execution failed: %s", error)
1791             sys.exit(1)
1792
1793     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1794         print "Warning: the specified device %s does not look like a removable usb device." % device
1795         f = raw_input("Do you really want to continue? y/N ")
1796         if f == "y" or f == "Y":
1797             pass
1798         else:
1799             sys.exit(1)
1800
1801
1802 def handle_compat_warning(device):
1803     """Backwards compatible checks
1804
1805     @device: device that should be checked"""
1806
1807     # make sure we can replace old grml2usb script and warn user when using old way of life:
1808     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1809         print "Warning: the semantics of grml2usb has changed."
1810         print "Instead of using grml2usb /path/to/iso %s you might" % device
1811         print "want to use grml2usb /path/to/iso /dev/... instead."
1812         print "Please check out the grml2usb manpage for details."
1813         f = raw_input("Do you really want to continue? y/N ")
1814         if f == "y" or f == "Y":
1815             pass
1816         else:
1817             sys.exit(1)
1818
1819
1820 def handle_logging():
1821     """Log handling and configuration"""
1822
1823     if options.verbose and options.quiet:
1824         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1825
1826     if options.verbose:
1827         FORMAT = "Debug: %(asctime)-15s %(message)s"
1828         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1829     elif options.quiet:
1830         FORMAT = "Critical: %(message)s"
1831         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1832     else:
1833         FORMAT = "%(message)s"
1834         logging.basicConfig(level=logging.INFO, format=FORMAT)
1835
1836
1837 def handle_bootloader(device):
1838     """wrapper for installing bootloader
1839
1840     @device: device where bootloader should be installed to"""
1841
1842     # Install bootloader only if not using the --copy-only option
1843     if options.copyonly:
1844         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1845     elif os.path.isdir(device):
1846         logging.info("Not installing bootloader as %s is a directory.", device)
1847     else:
1848         install_bootloader(device)
1849
1850
1851 def check_options(opts):
1852     """Check compability of provided user opts
1853
1854     @opts option dict from OptionParser
1855     """
1856     if opts.grubmbr and not opts.grub:
1857         logging.critical("Error: --grub-mbr requires --grub option.")
1858         sys.exit(1)
1859
1860
1861 def check_programs():
1862     """check if all needed programs are installed"""
1863     if options.grub:
1864         if not which("grub-install"):
1865             logging.critical("Fatal: grub-install not available (please install the "
1866                              + "grub package or drop the --grub option)")
1867             sys.exit(1)
1868
1869     if options.syslinux:
1870         if not which("syslinux"):
1871             logging.critical("Fatal: syslinux not available (please install the "
1872                              + "syslinux package or use the --grub option)")
1873             sys.exit(1)
1874
1875     if not which("rsync"):
1876         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1877         sys.exit(1)
1878
1879 def main():
1880     """Main function [make pylint happy :)]"""
1881
1882     if options.version:
1883         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1884         sys.exit(0)
1885
1886     if len(args) < 2:
1887         parser.error("invalid usage")
1888
1889     # log handling
1890     handle_logging()
1891
1892     # make sure we have the appropriate permissions
1893     check_uid_root()
1894
1895     check_options(options)
1896
1897     logging.info("Executing grml2usb version %s", PROG_VERSION)
1898
1899     if options.dryrun:
1900         logging.info("Running in simulation mode as requested via option dry-run.")
1901
1902     check_programs()
1903
1904     # specified arguments
1905     device = args[len(args) - 1]
1906     isos = args[0:len(args) - 1]
1907
1908     if not os.path.isdir(device):
1909         if device[-1:].isdigit():
1910             if int(device[-1:]) > 4 or device[-2:].isdigit():
1911                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1912                 sys.exit(1)
1913
1914     # provide upgrade path
1915     handle_compat_warning(device)
1916
1917     # check for vfat partition
1918     handle_vfat(device)
1919
1920     # main operation (like installing files)
1921     for iso in isos:
1922         install(iso, device)
1923
1924     # install mbr
1925     is_superfloppy = not device[-1:].isdigit()
1926     if is_superfloppy:
1927         logging.info("Detected superfloppy format - not installing MBR")
1928
1929     if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1930         handle_mbr(device)
1931
1932     handle_bootloader(device)
1933
1934     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1935
1936     for flavour in GRML_FLAVOURS:
1937         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1938
1939     # finally be politely :)
1940     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1941
1942
1943 if __name__ == "__main__":
1944     try:
1945         main()
1946     except KeyboardInterrupt:
1947         logging.info("Received KeyboardInterrupt")
1948         cleanup()
1949
1950 ## END OF FILE #################################################################
1951 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8