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