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