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