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