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