5feb238d2e6d6d1950b440335e9d1678d689fa4a
[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
1154     logo = search_file('logo.16', iso_mount)
1155     exec_rsync(logo, syslinux_target + 'logo.16')
1156
1157
1158     for ffile in ['f%d' % number for number in range(1, 11) ]:
1159         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1160
1161     loopback_cfg = search_file("loopback.cfg", iso_mount)
1162     if loopback_cfg:
1163         directory = os.path.dirname(loopback_cfg)
1164         directory = directory.replace(iso_mount, "")
1165         mkdir(os.path.join(target, directory))
1166         exec_rsync(loopback_cfg, target + os.path.sep + directory)
1167
1168     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1169     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1170     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1171         os.unlink(syslinux_target + 'ldlinux.sys')
1172
1173     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1174     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1175
1176     if not source_dir:
1177         logging.critical("Fatal: file default.cfg could not be found.")
1178         logging.critical("Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...")
1179         logging.critical("       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1180         logging.critical("       Please visit http://grml.org/grml2usb/#grml2usb-compat for further information.")
1181         raise
1182
1183     for expr in name, 'distri.cfg', \
1184         defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1185         'isoprompt.cfg', 'options.cfg', \
1186         'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1187         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1188
1189     for filename in glob.glob1(syslinux_target, "*.c32"):
1190         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1191
1192
1193     # copy the addons_*.cfg file to the new syslinux directory
1194     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1195
1196     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1197
1198
1199     grub_target = target + '/boot/grub/'
1200     execute(mkdir, grub_target)
1201
1202     if not os.path.isfile(GRML2USB_BASE + "/grub/splash.xpm.gz"):
1203         logging.critical("Error: %s/grub/splash.xpm.gz can not be read.", GRML2USB_BASE)
1204         logging.critical("Please make sure you've installed the grml2usb (Debian) package!")
1205         raise
1206     else:
1207         exec_rsync(GRML2USB_BASE + '/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz')
1208
1209     # grml splash in grub
1210     copy_if_exist(GRML2USB_BASE + "/grub/grml.png", grub_target + 'grml.png')
1211
1212     # font file for graphical bootsplash in grub
1213     copy_if_exist('/usr/share/grub/ascii.pf2', grub_target + 'ascii.pf2')
1214
1215     # always copy grub content as it might be useful
1216
1217     glob_and_copy(iso_mount + '/boot/grub/*.mod', grub_target)
1218     glob_and_copy(iso_mount + '/boot/grub/*.img', grub_target)
1219     glob_and_copy(iso_mount + '/boot/grub/stage*', grub_target)
1220
1221 def install_iso_files(grml_flavour, iso_mount, device, target):
1222     """Copy files from ISO to given target
1223
1224     @grml_flavour: name of grml flavour the configuration should be generated for
1225     @iso_mount: path where a grml ISO is mounted on
1226     @device: device/partition where bootloader should be installed to
1227     @target: path where grml's main files should be copied to"""
1228
1229     global GRML_DEFAULT
1230     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1231     if options.dryrun:
1232         return 0
1233     elif not options.bootloaderonly:
1234         logging.info("Copying files. This might take a while....")
1235         try:
1236             copy_system_files(grml_flavour, iso_mount, target)
1237             copy_grml_files(iso_mount, target)
1238         except CriticalException, error:
1239             logging.critical("Execution failed: %s", error)
1240             sys.exit(1)
1241
1242     if not options.skipaddons:
1243         if not search_file('addons', iso_mount):
1244             logging.info("Could not find addons, therefore not installing.")
1245         else:
1246             copy_addons(iso_mount, target)
1247
1248     if not options.copyonly:
1249         copy_bootloader_files(iso_mount, target, grml_flavour)
1250
1251         if not options.dryrun:
1252             handle_bootloader_config(grml_flavour, device, target)
1253
1254     # make sure we sync filesystems before returning
1255     proc = subprocess.Popen(["sync"])
1256     proc.wait()
1257
1258
1259 def get_flavour(flavour_str):
1260     """Returns the flavour of a grml version string
1261     """
1262     return re.match(r'[\w-]*', flavour_str).group()
1263
1264 def identify_grml_flavour(mountpath):
1265     """Get name of grml flavour
1266
1267     @mountpath: path where the grml ISO is mounted to
1268     @return: name of grml-flavour"""
1269
1270     version_file = search_file('grml-version', mountpath)
1271
1272     if version_file == "":
1273         logging.critical("Error: could not find grml-version file.")
1274         raise
1275
1276     flavours = []
1277     tmpfile = None
1278     try:
1279         tmpfile = open(version_file, 'r')
1280         for line in tmpfile.readlines():
1281             flavours.append(get_flavour(line))
1282     except TypeError, e:
1283         raise
1284     except Exception, e:
1285         logging.critical("Unexpected error: %s", e)
1286         raise
1287     finally:
1288         if tmpfile:
1289             tmpfile.close()
1290
1291     return flavours
1292
1293
1294 def modify_grub_config(filename):
1295     """Adjust bootoptions for a grub file
1296
1297     @filename: filename to modify
1298     """
1299     if options.removeoption:
1300         regexe = []
1301         for regex in options.removeoption:
1302             regexe.append(re.compile(r'%s' % regex))
1303
1304         option_re = re.compile(r'(.*/boot/release/.*linux26.*)')
1305
1306         for line in fileinput.input(filename, inplace=1):
1307             if regexe and option_re.search(line):
1308                 for regex in regexe:
1309                     line = regex.sub(' ', line)
1310
1311             sys.stdout.write(line)
1312
1313         fileinput.close()
1314
1315 def handle_grub2_config(grml_flavour, grub_target, bootopt):
1316     """Main handler for generating grub2 configuration
1317
1318     @grml_flavour: name of grml flavour the configuration should be generated for
1319     @grub_target: path of grub's configuration files
1320     @bootoptions: additional bootoptions that should be used by default"""
1321
1322     # grub2 config
1323     grub2_cfg = grub_target + 'grub.cfg'
1324     logging.debug("Creating grub2 configuration file (grub.cfg)")
1325
1326     global GRML_DEFAULT
1327
1328     # install main configuration only *once*, no matter how many ISOs we have:
1329     grub_flavour_is_default = False
1330     if os.path.isfile(grub2_cfg):
1331         string = open(grub2_cfg).readline()
1332         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1333         if not re.match(main_identifier, string):
1334             grub2_config_file = open(grub2_cfg, 'w')
1335             GRML_DEFAULT = grml_flavour
1336             grub_flavour_is_default = True
1337             grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1338             grub2_config_file.close()
1339     else:
1340         grub2_config_file = open(grub2_cfg, 'w')
1341         GRML_DEFAULT = grml_flavour
1342         grub_flavour_is_default = True
1343         grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1344         grub2_config_file.close()
1345
1346     # install flavour specific configuration only *once* as well
1347     grub_flavour_config = True
1348     if os.path.isfile(grub2_cfg):
1349         string = open(grub2_cfg).readlines()
1350         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1351         for line in string:
1352             if flavour.match(line):
1353                 grub_flavour_config = False
1354
1355     if grub_flavour_config:
1356         grub2_config_file = open(grub2_cfg, 'a')
1357         # display only if the grml flavour isn't the default
1358         if not grub_flavour_is_default:
1359             GRML_FLAVOURS.add(grml_flavour)
1360         grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, bootopt))
1361         grub2_config_file.close()
1362
1363     modify_grub_config(grub2_cfg)
1364
1365
1366 def get_bootoptions(grml_flavour):
1367     """Returns bootoptions for specific flavour
1368
1369     @grml_flavour: name of the grml_flavour
1370     """
1371     # do NOT write "None" in kernel cmdline
1372     if not options.bootoptions:
1373         bootopt = ""
1374     else:
1375         bootopt = " ".join(options.bootoptions)
1376     bootopt = bootopt.replace("%flavour", grml_flavour)
1377     return bootopt
1378
1379
1380 def handle_grub_config(grml_flavour, device, target):
1381     """Main handler for generating grub (v1 and v2) configuration
1382
1383     @grml_flavour: name of grml flavour the configuration should be generated for
1384     @device: device/partition where grub should be installed to
1385     @target: path of grub's configuration files"""
1386
1387     logging.debug("Generating grub configuration")
1388
1389     grub_target = target + '/boot/grub/'
1390
1391     bootopt = get_bootoptions(grml_flavour)
1392
1393     # write grub.cfg
1394     handle_grub2_config(grml_flavour, grub_target, bootopt)
1395
1396
1397 def initial_syslinux_config(target):
1398     """Generates intial syslinux configuration
1399
1400     @target path of syslinux's configuration files"""
1401
1402     target = target + "/"
1403     filename = target + "grmlmain.cfg"
1404     if os.path.isfile(target + "grmlmain.cfg"):
1405         return
1406     data = open(filename, "w")
1407     data.write(generate_main_syslinux_config())
1408     data.close()
1409
1410     filename = target + "hiddens.cfg"
1411     data = open(filename, "w")
1412     data.write("include hidden.cfg\n")
1413     data.close()
1414
1415 def add_entry_if_not_present(filename, entry):
1416     """Write entry into filename if entry is not already in the file
1417
1418     @filanme: name of the file
1419     @entry: data to write to the file
1420     """
1421     data = open(filename, "a+")
1422     for line in data:
1423         if line == entry:
1424             break
1425     else:
1426         data.write(entry)
1427
1428     data.close()
1429
1430 def get_flavour_filename(flavour):
1431     """Generate a iso9960 save filename out of the specified flavour
1432
1433     @flavour: grml flavour
1434     """
1435     return flavour.replace('-', '_')
1436
1437 def adjust_syslinux_bootoptions(src, flavour):
1438     """Adjust existing bootoptions of specified syslinux config to
1439     grml2usb specific ones, e.g. change the location of the kernel...
1440
1441     @src: config file to alter
1442     @flavour: grml flavour
1443     """
1444
1445     append_re = re.compile("^(\s*append.*/boot/release.*)$", re.I)
1446     boot_re = re.compile("/boot/([a-zA-Z0-9_]+/)+([a-zA-Z0-9._]+)")
1447     # flavour_re = re.compile("(label.*)(grml\w+)")
1448     default_re = re.compile("(default.cfg)")
1449     bootid_re = re.compile("bootid=[\w_-]+")
1450     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1451
1452     bootopt = get_bootoptions(flavour)
1453
1454     regexe = []
1455     option_re = None
1456     if options.removeoption:
1457         option_re = re.compile(r'/boot/release/.*/initrd.gz')
1458
1459         for regex in options.removeoption:
1460             regexe.append(re.compile(r'%s' % regex))
1461
1462     for line in fileinput.input(src, inplace=1):
1463         line = boot_re.sub(r'/boot/release/%s/\2 ' % flavour.replace('-', ''), line)
1464         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1465         line = default_re.sub(r'%s-\1' % flavour, line)
1466         line = bootid_re.sub('', line)
1467         line = live_media_path_re.sub('', line)
1468         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1469         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1470         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1471         if option_re and option_re.search(line):
1472             for regex in regexe:
1473                 line = regex.sub(' ', line)
1474         sys.stdout.write(line)
1475     fileinput.close()
1476
1477 def adjust_labels(src, replacement):
1478     """Adjust the specified labels in the syslinux config file src with
1479     specified replacement
1480     """
1481     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1482     for line in fileinput.input(src, inplace=1):
1483         line = label_re.sub(replacement, line)
1484         sys.stdout.write(line)
1485     fileinput.close()
1486
1487
1488 def add_syslinux_entry(filename, grml_flavour):
1489     """Add includes for a specific grml_flavour to the specified filename
1490
1491     @filename: syslinux config file
1492     @grml_flavour: grml flavour to add
1493     """
1494
1495     entry_filename = "option_%s.cfg" % grml_flavour
1496     entry = "include %s\n" % entry_filename
1497
1498     add_entry_if_not_present(filename, entry)
1499     path = os.path.dirname(filename)
1500
1501     data = open(path + "/" + entry_filename, "w")
1502     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1503     data.close()
1504
1505 def modify_filenames(grml_flavour, target, filenames):
1506     """Replace the standarf filenames with the new ones
1507
1508     @grml_flavour: grml-flavour strin
1509     @target: directory where the files are located
1510     @filenames: list of filenames to alter
1511     """
1512     grml_filename = grml_flavour.replace('-', '_')
1513     for filename in filenames:
1514         old_filename = "%s/%s" % (target, filename)
1515         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1516         os.rename(old_filename, new_filename)
1517
1518
1519 def remove_default_entry(filename):
1520     """Remove the default entry from specified syslinux file
1521
1522     @filename: syslinux config file
1523     """
1524     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1525     for line in fileinput.input(filename, inplace=1):
1526         if default_re.match(line):
1527             continue
1528         sys.stdout.write(line)
1529     fileinput.close()
1530
1531
1532 def handle_syslinux_config(grml_flavour, target):
1533     """Main handler for generating syslinux configuration
1534
1535     @grml_flavour: name of grml flavour the configuration should be generated for
1536     @target: path of syslinux's configuration files"""
1537
1538     logging.debug("Generating syslinux configuration")
1539     syslinux_target = target + '/boot/syslinux/'
1540     # should be present via  copy_bootloader_files(), but make sure it exits:
1541     execute(mkdir, syslinux_target)
1542     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1543
1544
1545     # install main configuration only *once*, no matter how many ISOs we have:
1546     syslinux_config_file = open(syslinux_cfg, 'w')
1547     syslinux_config_file.write("TIMEOUT 300\n")
1548     syslinux_config_file.write("include vesamenu.cfg\n")
1549     syslinux_config_file.close()
1550
1551     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1552     prompt_name.write('menu label S^yslinux prompt\n')
1553     prompt_name.close()
1554
1555     initial_syslinux_config(syslinux_target)
1556     flavour_filename = grml_flavour.replace('-', '_')
1557
1558     if search_file('default.cfg', syslinux_target):
1559         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1560
1561     filename = search_file("new_hidden.cfg", syslinux_target)
1562
1563
1564     # process hidden file
1565     if not search_file("hidden.cfg", syslinux_target):
1566         new_hidden = syslinux_target + "hidden.cfg"
1567         os.rename(filename, new_hidden)
1568         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1569     else:
1570         new_hidden_file =  "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1571         os.rename(filename, new_hidden_file)
1572         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1573         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1574         entry = 'include %s_hidden.cfg\n' % flavour_filename
1575         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1576
1577
1578
1579     new_default = "%s_default.cfg" % (flavour_filename)
1580     entry = 'include %s\n' % new_default
1581     defaults_file = '%s/defaults.cfg' % syslinux_target
1582     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1583     new_grml_cfg = "%s/%s_grml.cfg" % ( syslinux_target, flavour_filename)
1584
1585     if os.path.isfile(defaults_file):
1586
1587         # remove default menu entry in menu
1588         remove_default_entry(new_default_with_path)
1589
1590         # adjust all labels for additional isos
1591         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1592         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1593
1594     # always adjust bootoptions
1595     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1596     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1597
1598     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1599
1600     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1601
1602
1603 def handle_bootloader_config(grml_flavour, device, target):
1604     """Main handler for generating bootloader's configuration
1605
1606     @grml_flavour: name of grml flavour the configuration should be generated for
1607     @device: device/partition where bootloader should be installed to
1608     @target: path of bootloader's configuration files"""
1609
1610     global UUID
1611     UUID = get_uuid(target)
1612     if options.skipsyslinuxconfig:
1613         logging.info("Skipping generation of syslinux configuration as requested.")
1614     else:
1615         try:
1616             handle_syslinux_config(grml_flavour, target)
1617         except CriticalException, error:
1618             logging.critical("Fatal: %s", error)
1619             sys.exit(1)
1620
1621     if options.skipgrubconfig:
1622         logging.info("Skipping generation of grub configuration as requested.")
1623     else:
1624         try:
1625             handle_grub_config(grml_flavour, device, target)
1626         except CriticalException, error:
1627             logging.critical("Fatal: %s", error)
1628             sys.exit(1)
1629
1630
1631
1632 def install(image, device):
1633     """Install a grml image to the specified device
1634
1635     @image: directory or is file
1636     @device: partition or directory to install the device
1637     """
1638     iso_mountpoint = image
1639     remove_image_mountpoint = False
1640     if os.path.isdir(image):
1641         logging.info("Using %s as install base", image)
1642     else:
1643         logging.info("Using ISO %s", image)
1644         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1645         register_tmpfile(iso_mountpoint)
1646         remove_image_mountpoint = True
1647         try:
1648             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1649         except CriticalException, error:
1650             logging.critical("Fatal: %s", error)
1651             sys.exit(1)
1652
1653     try:
1654         install_grml(iso_mountpoint, device)
1655     finally:
1656         if remove_image_mountpoint:
1657             try:
1658                 remove_mountpoint(iso_mountpoint)
1659             except CriticalException, error:
1660                 logging.critical("Fatal: %s", error)
1661                 cleanup()
1662
1663
1664
1665 def install_grml(mountpoint, device):
1666     """Main logic for copying files of the currently running grml system.
1667
1668     @mountpoin: directory where currently running live system resides (usually /live/image)
1669     @device: partition where the specified ISO should be installed to"""
1670
1671     device_mountpoint = device
1672     if os.path.isdir(device):
1673         logging.info("Specified device is not a directory, therefore not mounting.")
1674         remove_device_mountpoint = False
1675     else:
1676         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1677         register_tmpfile(device_mountpoint)
1678         remove_device_mountpoint = True
1679         try:
1680             check_for_fat(device)
1681             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1682         except CriticalException, error:
1683             try:
1684                 mount(device, device_mountpoint, "")
1685             except CriticalException, error:
1686                 logging.critical("Fatal: %s", error)
1687                 raise
1688     try:
1689         grml_flavours = identify_grml_flavour(mountpoint)
1690         for flavour in set(grml_flavours):
1691             if not flavour:
1692                 logging.warning("No valid flavour found, please check your iso")
1693             logging.info("Identified grml flavour \"%s\".", flavour)
1694             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1695             GRML_FLAVOURS.add(flavour)
1696     finally:
1697         if remove_device_mountpoint:
1698             remove_mountpoint(device_mountpoint)
1699
1700 def remove_mountpoint(mountpoint):
1701     """remove a registred mountpoint
1702     """
1703
1704     try:
1705         unmount(mountpoint, "")
1706         if os.path.isdir(mountpoint):
1707             os.rmdir(mountpoint)
1708             unregister_tmpfile(mountpoint)
1709     except CriticalException, error:
1710         logging.critical("Fatal: %s", error)
1711         cleanup()
1712
1713 def handle_mbr(device):
1714     """Main handler for installing master boot record (MBR)
1715
1716     @device: device where the MBR should be installed to"""
1717
1718     if options.dryrun:
1719         logging.info("Would install MBR")
1720         return 0
1721
1722     if device[-1:].isdigit():
1723         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1724         partition_number = int(device[-1:]) - 1
1725     else:
1726         logging.warn("Could not detect partition number, not activating partition")
1727         partition_number = None
1728
1729     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1730     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1731     if mbr_device == "/dev/loop":
1732         mbr_device = device
1733         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1734
1735     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1736     if options.syslinuxmbr:
1737         mbrcode = '/usr/lib/syslinux/mbr.bin'
1738     elif options.mbrmenu:
1739         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1740
1741     try:
1742         install_mbr(mbrcode, mbr_device, partition_number, True)
1743     except IOError, error:
1744         logging.critical("Execution failed: %s", error)
1745         sys.exit(1)
1746     except Exception, error:
1747         logging.critical("Execution failed: %s", error)
1748         sys.exit(1)
1749
1750
1751 def handle_vfat(device):
1752     """Check for FAT specific settings and options
1753
1754     @device: device that should checked / formated"""
1755
1756     # make sure we have mkfs.vfat available
1757     if options.fat16:
1758         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1759             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1760             logging.critical('Please make sure to install dosfstools.')
1761             sys.exit(1)
1762
1763         if options.force:
1764             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1765         else:
1766             # make sure the user is aware of what he is doing
1767             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1768             if f == "y" or f == "Y":
1769                 logging.info("Note: you can skip this question using the option --force")
1770             else:
1771                 sys.exit(1)
1772         try:
1773             mkfs_fat16(device)
1774         except CriticalException, error:
1775             logging.critical("Execution failed: %s", error)
1776             sys.exit(1)
1777
1778     # check for vfat filesystem
1779     if device is not None and not os.path.isdir(device) and options.syslinux:
1780         try:
1781             check_for_fat(device)
1782         except CriticalException, error:
1783             logging.critical("Execution failed: %s", error)
1784             sys.exit(1)
1785
1786     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1787         print "Warning: the specified device %s does not look like a removable usb device." % device
1788         f = raw_input("Do you really want to continue? y/N ")
1789         if f == "y" or f == "Y":
1790             pass
1791         else:
1792             sys.exit(1)
1793
1794
1795 def handle_compat_warning(device):
1796     """Backwards compatible checks
1797
1798     @device: device that should be checked"""
1799
1800     # make sure we can replace old grml2usb script and warn user when using old way of life:
1801     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1802         print "Warning: the semantics of grml2usb has changed."
1803         print "Instead of using grml2usb /path/to/iso %s you might" % device
1804         print "want to use grml2usb /path/to/iso /dev/... instead."
1805         print "Please check out the grml2usb manpage for details."
1806         f = raw_input("Do you really want to continue? y/N ")
1807         if f == "y" or f == "Y":
1808             pass
1809         else:
1810             sys.exit(1)
1811
1812
1813 def handle_logging():
1814     """Log handling and configuration"""
1815
1816     if options.verbose and options.quiet:
1817         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1818
1819     if options.verbose:
1820         FORMAT = "Debug: %(asctime)-15s %(message)s"
1821         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1822     elif options.quiet:
1823         FORMAT = "Critical: %(message)s"
1824         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1825     else:
1826         FORMAT = "%(message)s"
1827         logging.basicConfig(level=logging.INFO, format=FORMAT)
1828
1829
1830 def handle_bootloader(device):
1831     """wrapper for installing bootloader
1832
1833     @device: device where bootloader should be installed to"""
1834
1835     # Install bootloader only if not using the --copy-only option
1836     if options.copyonly:
1837         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1838     elif os.path.isdir(device):
1839         logging.info("Not installing bootloader as %s is a directory.", device)
1840     else:
1841         install_bootloader(device)
1842
1843
1844 def check_options(opts):
1845     """Check compability of provided user opts
1846
1847     @opts option dict from OptionParser
1848     """
1849     if opts.grubmbr and not opts.grub:
1850         logging.critical("Error: --grub-mbr requires --grub option.")
1851         sys.exit(1)
1852
1853
1854 def check_programs():
1855     """check if all needed programs are installed"""
1856     if options.grub:
1857         if not which("grub-install"):
1858             logging.critical("Fatal: grub-install not available (please install the "
1859                              + "grub package or drop the --grub option)")
1860             sys.exit(1)
1861
1862     if options.syslinux:
1863         if not which("syslinux"):
1864             logging.critical("Fatal: syslinux not available (please install the "
1865                              + "syslinux package or use the --grub option)")
1866             sys.exit(1)
1867
1868     if not which("rsync"):
1869         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1870         sys.exit(1)
1871
1872 def main():
1873     """Main function [make pylint happy :)]"""
1874
1875     if options.version:
1876         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1877         sys.exit(0)
1878
1879     if len(args) < 2:
1880         parser.error("invalid usage")
1881
1882     # log handling
1883     handle_logging()
1884
1885     # make sure we have the appropriate permissions
1886     check_uid_root()
1887
1888     check_options(options)
1889
1890     logging.info("Executing grml2usb version %s", PROG_VERSION)
1891
1892     if options.dryrun:
1893         logging.info("Running in simulation mode as requested via option dry-run.")
1894
1895     check_programs()
1896
1897     # specified arguments
1898     device = args[len(args) - 1]
1899     isos = args[0:len(args) - 1]
1900
1901     if not os.path.isdir(device):
1902         if device[-1:].isdigit():
1903             if int(device[-1:]) > 4 or device[-2:].isdigit():
1904                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1905                 sys.exit(1)
1906
1907     # provide upgrade path
1908     handle_compat_warning(device)
1909
1910     # check for vfat partition
1911     handle_vfat(device)
1912
1913     # main operation (like installing files)
1914     for iso in isos:
1915         install(iso, device)
1916
1917     # install mbr
1918     is_superfloppy = not device[-1:].isdigit()
1919     if is_superfloppy:
1920         logging.info("Detected superfloppy format - not installing MBR")
1921
1922     if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1923         handle_mbr(device)
1924
1925     handle_bootloader(device)
1926
1927     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1928
1929     for flavour in GRML_FLAVOURS:
1930         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1931
1932     # finally be politely :)
1933     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1934
1935
1936 if __name__ == "__main__":
1937     try:
1938         main()
1939     except KeyboardInterrupt:
1940         logging.info("Received KeyboardInterrupt")
1941         cleanup()
1942
1943 ## END OF FILE #################################################################
1944 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8