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