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