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