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