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