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