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