Release new version 0.9.27.
[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.27"
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 --force --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         try:
703             install_grub(device)
704         except CriticalException, error:
705             logging.critical("Fatal: %s", error)
706             cleanup()
707             sys.exit(1)
708     else:
709         try:
710             install_syslinux(device)
711         except CriticalException, error:
712             logging.critical("Fatal: %s", error)
713             cleanup()
714             sys.exit(1)
715
716
717 def execute_lilo(lilo, device):
718     """execute lilo for activating the partitions in the MBR
719
720     @lilo: path of lilo executable
721     @device: device where lilo should be executed on"""
722
723     # to support -A for extended partitions:
724     logging.info("Activating partitions in MBR via lilo")
725     logging.debug("%s -S /dev/null -M %s ext", lilo, device)
726     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
727     proc.wait()
728     if proc.returncode != 0:
729         raise Exception("error executing lilo")
730
731     # activate partition:
732     logging.debug("%s -S /dev/null -A %s 1", lilo, device)
733     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
734     proc.wait()
735     if proc.returncode != 0:
736         raise Exception("error executing lilo")
737
738
739 def install_syslinux_mbr(device):
740     """install syslinux's master boot record (MBR) on the specified device
741
742     @device: device where MBR of syslinux should be installed to"""
743
744     # make sure we have syslinux available
745     if not which("syslinux") and not options.copyonly:
746         raise Exception("syslinux not available (either install it or consider using the --grub option)")
747
748     # lilo's mbr is broken, use the one from syslinux instead:
749     if not os.path.isfile("/usr/lib/syslinux/mbr.bin"):
750         raise Exception("/usr/lib/syslinux/mbr.bin can not be read")
751
752     logging.info("Installing syslinux MBR")
753     logging.debug("cat /usr/lib/syslinux/mbr.bin > %s", device)
754     try:
755         retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
756         if retcode < 0:
757             logging.critical("Error copying MBR to device (%s)", retcode)
758     except OSError, error:
759         logging.critical("Execution failed: %s", error)
760
761
762 def install_mir_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
763     """install 'mbr' master boot record (MBR) on a device
764
765     Retrieve the partition table from "device", install an MBR from the
766     "mbrtemplate" file, set the "partition" (0..3) active, and install the
767     result back to "device".
768
769     @mbrtemplate: default MBR file
770
771     @device: name of a file assumed to be a hard disc (or USB stick) image, or
772     something like "/dev/sdb"
773
774     @partition: must be a number between 0 and 3, inclusive
775
776     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
777     ismirbsdmbr) bytes.
778
779     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
780     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
781     false then leave the mirbsdmbr specific flag set to FFh, set all
782     active flags to 0 and set the active flag of the partition to 80h.  Note:
783     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
784     the active flag."""
785
786     logging.info("Installing default MBR")
787
788     if not os.path.isfile(mbrtemplate):
789         logging.critical("Error: %s can not be read.", mbrtemplate)
790         raise CriticalException("Error installing MBR (either try --syslinux-mbr or install missing file?)")
791
792     if (partition < 0) or (partition > 3):
793         raise ValueError("partition must be between 0 and 3")
794
795     if ismirbsdmbr:
796         nmbrbytes = 439
797     else:
798         nmbrbytes = 440
799
800     tmpf = tempfile.NamedTemporaryFile()
801
802     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
803     proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
804                             stderr=file(os.devnull, "r+"))
805     proc.wait()
806     if proc.returncode != 0:
807         raise Exception("error executing dd (first run)")
808
809     logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc", mbrtemplate,
810                   tmpf.name, nmbrbytes)
811     proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes,
812                              "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
813     proc.wait()
814     if proc.returncode != 0:
815         raise Exception("error executing dd (second run)")
816
817     mbrcode = tmpf.file.read(512)
818     if len(mbrcode) < 512:
819         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
820
821     if ismirbsdmbr:
822         mbrcode = mbrcode[0:439] + chr(partition) + \
823                 mbrcode[440:510] + "\x55\xAA"
824     else:
825         actives = ["\x00", "\x00", "\x00", "\x00"]
826         actives[partition] = "\x80"
827         mbrcode = mbrcode[0:446] + actives[0] + \
828                 mbrcode[447:462] + actives[1] + \
829                 mbrcode[463:478] + actives[2] + \
830                 mbrcode[479:494] + actives[3] + \
831                 mbrcode[495:510] + "\x55\xAA"
832
833     tmpf.file.seek(0)
834     tmpf.file.truncate()
835     tmpf.file.write(mbrcode)
836     tmpf.file.close()
837
838     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device)
839     proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % device, "bs=512", "count=1",
840                              "conv=notrunc"], stderr=file(os.devnull, "r+"))
841     proc.wait()
842     if proc.returncode != 0:
843         raise Exception("error executing dd (third run)")
844     del tmpf
845
846
847 def handle_syslinux_mbr(device):
848     """Install syslinux master boot record on given device
849
850     @device: device where MBR should be installed to"""
851
852     if not is_writeable(device):
853         raise IOError("device not writeable for user")
854
855     # try to use system's lilo
856     if which("lilo"):
857         lilo = which("lilo")
858     else:
859         # otherwise fall back to our static version
860         from platform import architecture
861         if architecture()[0] == '64bit':
862             lilo = GRML2USB_BASE + '/lilo/lilo.static.amd64'
863         else:
864             lilo = GRML2USB_BASE + '/lilo/lilo.static.i386'
865     # finally prefer a specified lilo executable
866     if options.lilobin:
867         lilo = options.lilobin
868
869     if not is_exe(lilo):
870         raise Exception("lilo executable can not be execute")
871
872     if options.dryrun:
873         logging.info("Would install MBR running lilo and using syslinux.")
874         return 0
875
876     execute_lilo(lilo, device)
877     install_syslinux_mbr(device)
878
879
880 def is_writeable(device):
881     """Check if the device is writeable for the current user
882
883     @device: partition where bootloader should be installed to"""
884
885     if not device:
886         return False
887         #raise Exception("no device for checking write permissions")
888
889     if not os.path.exists(device):
890         return False
891
892     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
893
894
895 def mount(source, target, mount_options):
896     """Mount specified source on given target
897
898     @source: name of device/ISO that should be mounted
899     @target: directory where the ISO should be mounted to
900     @options: mount specific options"""
901
902     # note: options.dryrun does not work here, as we have to
903     # locate files and identify the grml flavour
904
905     for x in file('/proc/mounts').readlines():
906         if x.startswith(source):
907             raise CriticalException("Error executing mount: %s already mounted - " % source
908                                     + "please unmount before invoking grml2usb")
909
910     if os.path.isdir(source):
911         logging.debug("Source %s is not a device, therefore not mounting.", source)
912         return 0
913
914     logging.debug("mount %s %s %s", mount_options, source, target)
915     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
916     proc.wait()
917     if proc.returncode != 0:
918         raise CriticalException("Error executing mount (no filesystem on the partition?)")
919     else:
920         logging.debug("register_mountpoint(%s)", target)
921         register_mountpoint(target)
922
923
924 def unmount(target, unmount_options):
925     """Unmount specified target
926
927     @target: target where something is mounted on and which should be unmounted
928     @options: options for umount command"""
929
930     # make sure we unmount only already mounted targets
931     target_unmount = False
932     mounts = open('/proc/mounts').readlines()
933     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
934     for line in mounts:
935         if re.match(mountstring, line):
936             target_unmount = True
937
938     if not target_unmount:
939         logging.debug("%s not mounted anymore", target)
940     else:
941         logging.debug("umount %s %s", list(unmount_options), target)
942         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
943         proc.wait()
944         if proc.returncode != 0:
945             raise Exception("Error executing umount")
946         else:
947             logging.debug("unregister_mountpoint(%s)", target)
948             unregister_mountpoint(target)
949
950
951 def check_for_usbdevice(device):
952     """Check whether the specified device is a removable USB device
953
954     @device: device name, like /dev/sda1 or /dev/sda
955     """
956
957     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
958     # newer systems:
959     usbdev = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
960     if not os.path.isfile(usbdev):
961         # Ubuntu with kernel 2.6.24 for example:
962         usbdev = os.path.realpath('/sys/block/' + usbdevice + '/removable')
963
964     if os.path.isfile(usbdev):
965         is_usb = open(usbdev).readline()
966         if is_usb.find("1"):
967             return 0
968
969     return 1
970
971
972 def check_for_fat(partition):
973     """Check whether specified partition is a valid VFAT/FAT16 filesystem
974
975     @partition: device name of partition"""
976
977     if not os.access(partition, os.R_OK):
978         raise CriticalException("Failed to read device %s"
979                 " (wrong UID/permissions or device/directory not present?)" % partition)
980
981     try:
982         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
983                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
984         filesystem = udev_info.communicate()[0].rstrip()
985
986         if filesystem != "vfat":
987             raise CriticalException(
988                     "Partition %s does not contain a FAT16 filesystem. "
989                     "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
990
991     except OSError:
992         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
993
994
995 def mkdir(directory):
996     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
997
998     # just silently pass as it's just fine it the directory exists
999     if not os.path.isdir(directory):
1000         try:
1001             os.makedirs(directory)
1002         # pylint: disable-msg=W0704
1003         except OSError:
1004             pass
1005
1006
1007 def exec_rsync(source, target):
1008     """Simple wrapper around rsync to install files
1009
1010     @source: source file/directory
1011     @target: target file/directory"""
1012     logging.debug("Source: %s / Target: %s", source, target)
1013     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
1014     proc.wait()
1015     if proc.returncode == 12:
1016         logging.critical("Fatal: No space left on device")
1017         cleanup()
1018         sys.exit(1)
1019
1020     if proc.returncode != 0:
1021         logging.critical("Fatal: could not install %s", source)
1022         cleanup()
1023         sys.exit(1)
1024
1025
1026 def write_uuid(target_file):
1027     """Generates an returns uuid and write it to the specified file
1028
1029     @target_file: filename to write the uuid to
1030     """
1031
1032     fileh = open(target_file, 'w')
1033     uid = str(uuid.uuid4())
1034     fileh.write(uid)
1035     fileh.close()
1036     return uid
1037
1038
1039 def get_uuid(target):
1040     """Get the uuid of the specified target. Will generate an uuid if none exist.
1041
1042     @target: directory/mountpoint containing the grml layout
1043     """
1044
1045     conf_target = target + "/conf/"
1046     uuid_file_name = conf_target + "/bootid.txt"
1047     if os.path.isdir(conf_target):
1048         if os.path.isfile(uuid_file_name):
1049             uuid_file = open(uuid_file_name, 'r')
1050             uid = uuid_file.readline().strip()
1051             uuid_file.close()
1052             return uid
1053         else:
1054             return write_uuid(uuid_file_name)
1055     else:
1056         execute(mkdir, conf_target)
1057         return write_uuid(uuid_file_name)
1058
1059
1060 def copy_system_files(grml_flavour, iso_mount, target):
1061     """copy grml's main files (like squashfs, kernel and initrd) to a given target
1062
1063     @grml_flavour: name of grml flavour the configuration should be generated for
1064     @iso_mount: path where a grml ISO is mounted on
1065     @target: path where grml's main files should be copied to"""
1066
1067     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
1068     if squashfs is None:
1069         logging.critical("Fatal: squashfs file not found")
1070         raise CriticalException("error locating squashfs file")
1071     else:
1072         squashfs_target = target + '/live/' + grml_flavour + '/'
1073         execute(mkdir, squashfs_target)
1074     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
1075
1076     for prefix in grml_flavour + "/", "":
1077         filesystem_module = search_file(prefix + 'filesystem.module', iso_mount)
1078         if filesystem_module:
1079             break
1080     if filesystem_module is None:
1081         logging.critical("Fatal: filesystem.module not found")
1082         raise CriticalException("error locating filesystem.module file")
1083     else:
1084         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
1085
1086
1087     release_path = 'boot/release/' + grml_flavour.replace('-', '')
1088     release_target = target + "/" + release_path
1089     execute(mkdir, release_target)
1090
1091     prefix = ""
1092     if os.path.isdir(iso_mount + '/boot/release'):
1093         prefix = release_path + '/'
1094
1095     kernel = search_file(prefix + 'linux26', iso_mount)
1096     if kernel is None:
1097         logging.critical("Fatal kernel not found")
1098         raise CriticalException("error locating kernel file")
1099     else:
1100         exec_rsync(kernel, release_target + '/linux26')
1101
1102     initrd = search_file(prefix + 'initrd.gz', iso_mount)
1103     if initrd is None:
1104         logging.critical("Fatal: initrd not found")
1105         raise CriticalException("error locating initrd file")
1106     else:
1107         exec_rsync(initrd, release_target + '/initrd.gz')
1108
1109
1110 def update_grml_versions(iso_mount, target):
1111     """Update the grml version file on a cd
1112     Returns true if version was updated successfully,
1113     False if grml-version does not exist yet on the mountpoint
1114
1115     @iso_mount: string of the iso mount point
1116     @target: path of the target mount point
1117     """
1118     grml_target = target + '/grml/'
1119     new_grml_version = search_file('grml-version', grml_target)
1120     if new_grml_version:
1121         orig_grml_version = search_file('grml-version', iso_mount)
1122         if not orig_grml_version:
1123             logging.warn("Warning: %s could not be found - can not install it", orig_grml_version)
1124             return False
1125         try:
1126             new_file = open(new_grml_version, 'a+')
1127             new_flavours = [ get_flavour(l) for l in new_file.readlines() ]
1128
1129             old_file = open(orig_grml_version, 'r')
1130             old_lines = old_file.readlines()
1131             for line in old_lines:
1132                 if not get_flavour(line) in new_flavours:
1133                     new_file.write(line)
1134         except IOError:
1135             logging.warn("Warning: Could not write file")
1136         finally:
1137             new_file.close()
1138             old_file.close()
1139         return True
1140     else:
1141         return False
1142
1143 def copy_grml_files(iso_mount, target):
1144     """copy some minor grml files to a given target
1145
1146     @iso_mount: path where a grml ISO is mounted on
1147     @target: path where grml's main files should be copied to"""
1148
1149     grml_target = target + '/grml/'
1150     execute(mkdir, grml_target)
1151
1152     copy_files = [ 'grml-cheatcodes.txt', 'LICENSE.txt', 'md5sums', 'README.txt' ]
1153     # handle grml-version
1154     if not update_grml_versions(iso_mount, target):
1155         copy_files.append('grml-version')
1156
1157     for myfile in copy_files:
1158         grml_file = search_file(myfile, iso_mount)
1159         if grml_file is None:
1160             logging.warn("Warning: myfile %s could not be found - can not install it", myfile)
1161         else:
1162             exec_rsync(grml_file, grml_target + myfile)
1163
1164     grml_web_target = grml_target + '/web/'
1165     execute(mkdir, grml_web_target)
1166
1167     for myfile in 'index.html', 'style.css':
1168         grml_file = search_file(myfile, iso_mount)
1169         if grml_file is None:
1170             logging.warn("Warning: myfile %s could not be found - can not install it")
1171         else:
1172             exec_rsync(grml_file, grml_web_target + myfile)
1173
1174     grml_webimg_target = grml_web_target + '/images/'
1175     execute(mkdir, grml_webimg_target)
1176
1177     for myfile in 'button.png', 'favicon.png', 'linux.jpg', 'logo.png':
1178         grml_file = search_file(myfile, iso_mount)
1179         if grml_file is None:
1180             logging.warn("Warning: myfile %s could not be found - can not install it")
1181         else:
1182             exec_rsync(grml_file, grml_webimg_target + myfile)
1183
1184
1185 def handle_addon_copy(filename, dst, iso_mount):
1186     """handle copy of optional addons
1187
1188     @filename: filename of the addon
1189     @dst: destination directory
1190     @iso_mount: location of the iso mount
1191     """
1192     file_location = search_file(filename, iso_mount)
1193     if file_location is None:
1194         logging.warn("Warning: %s not found (that's fine if you don't need it)",  filename)
1195     else:
1196         exec_rsync(file_location, dst + filename)
1197
1198
1199 def copy_addons(iso_mount, target):
1200     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1201
1202     @iso_mount: path where a grml ISO is mounted on
1203     @target: path where grml's main files should be copied to"""
1204
1205     addons = target + '/boot/addons/'
1206     execute(mkdir, addons)
1207
1208     # grub all-in-one image
1209     handle_addon_copy('allinone.img', addons, iso_mount)
1210
1211     # bsd imag
1212     handle_addon_copy('bsd4grml', addons, iso_mount)
1213
1214     handle_addon_copy('balder10.imz', addons, iso_mount)
1215
1216     # install hdt and pci.ids only when using syslinux (grub doesn't support it)
1217     if options.syslinux:
1218         # hdt (hardware detection tool) image
1219         hdtimg = search_file('hdt.c32', iso_mount)
1220         if hdtimg:
1221             exec_rsync(hdtimg, addons + '/hdt.c32')
1222
1223         # pci.ids file
1224         picids = search_file('pci.ids', iso_mount)
1225         if picids:
1226             exec_rsync(picids, addons + '/pci.ids')
1227
1228     # memdisk image
1229     handle_addon_copy('memdisk', addons, iso_mount)
1230
1231     # memtest86+ image
1232     handle_addon_copy('memtest', addons, iso_mount)
1233
1234     # gpxe.lkrn
1235     handle_addon_copy('gpxe.lkrn', addons, iso_mount)
1236
1237
1238 def glob_and_copy(filepattern, dst):
1239     """Glob on specified filepattern and copy the result to dst
1240
1241     @filepattern: globbing pattern
1242     @dst: target directory
1243     """
1244     for name in glob.glob(filepattern):
1245         copy_if_exist(name, dst)
1246
1247 def search_and_copy(filename, search_path, dst):
1248     """Search for the specified filename at searchpath and copy it to dst
1249
1250     @filename: filename to look for
1251     @search_path: base search file
1252     @dst: destionation to copy the file to
1253     """
1254     file_location = search_file(filename, search_path)
1255     copy_if_exist(file_location, dst)
1256
1257 def copy_if_exist(filename, dst):
1258     """Copy filename to dst if filename is set.
1259
1260     @filename: a filename
1261     @dst: dst file
1262     """
1263     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1264         exec_rsync(filename, dst)
1265
1266 def copy_bootloader_files(iso_mount, target, grml_flavour):
1267     """Copy grml's bootloader files to a given target
1268
1269     @iso_mount: path where a grml ISO is mounted on
1270     @target: path where grml's main files should be copied to
1271     @grml_flavour: name of the current processed grml_flavour
1272     """
1273
1274     syslinux_target = target + '/boot/syslinux/'
1275     execute(mkdir, syslinux_target)
1276
1277     grub_target = target + '/boot/grub/'
1278     execute(mkdir, grub_target)
1279
1280
1281     logo = search_file('logo.16', iso_mount)
1282     exec_rsync(logo, syslinux_target + 'logo.16')
1283
1284
1285     for ffile in ['f%d' % number for number in range(1, 11) ]:
1286         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1287
1288     loopback_cfg = search_file("loopback.cfg", iso_mount)
1289     if loopback_cfg:
1290         directory = os.path.dirname(loopback_cfg)
1291         directory = directory.replace(iso_mount, "")
1292         mkdir(os.path.join(target, directory))
1293         exec_rsync(loopback_cfg, target + os.path.sep + directory)
1294
1295     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1296     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1297     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1298         os.unlink(syslinux_target + 'ldlinux.sys')
1299
1300     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1301     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1302
1303     if not source_dir:
1304         logging.critical("Fatal: file default.cfg could not be found.")
1305         logging.critical("Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...")
1306         logging.critical("       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1307         logging.critical("       Please visit http://grml.org/grml2usb/#grml2usb-compat for further information.")
1308         raise
1309
1310     for expr in name, 'distri.cfg', \
1311         defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1312         'isoprompt.cfg', 'options.cfg', \
1313         'prompt.cfg', 'vesamenu.c32', 'vesamenu.cfg', 'grml.png', '*.c32':
1314         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1315
1316     # copy the addons_*.cfg file to the new syslinux directory
1317     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1318
1319     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1320
1321
1322     grub_target = target + '/boot/grub/'
1323     execute(mkdir, grub_target)
1324
1325     if not os.path.isfile(GRML2USB_BASE + "/grub/splash.xpm.gz"):
1326         logging.critical("Error: %s/grub/splash.xpm.gz can not be read.", GRML2USB_BASE)
1327         logging.critical("Please make sure you've installed the grml2usb (Debian) package!")
1328         raise
1329     else:
1330         exec_rsync(GRML2USB_BASE + '/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz')
1331
1332     # grml splash in grub
1333     copy_if_exist(GRML2USB_BASE + "/grub/grml.png", grub_target + 'grml.png')
1334
1335     # font file for graphical bootsplash in grub
1336     copy_if_exist('/usr/share/grub/ascii.pf2', grub_target + 'ascii.pf2')
1337
1338     # always copy grub content as it might be useful
1339
1340     glob_and_copy(iso_mount + '/boot/grub/*.mod', grub_target)
1341     glob_and_copy(iso_mount + '/boot/grub/*.img', grub_target)
1342     glob_and_copy(iso_mount + '/boot/grub/stage*', grub_target)
1343
1344 def install_iso_files(grml_flavour, iso_mount, device, target):
1345     """Copy files from ISO to given target
1346
1347     @grml_flavour: name of grml flavour the configuration should be generated for
1348     @iso_mount: path where a grml ISO is mounted on
1349     @device: device/partition where bootloader should be installed to
1350     @target: path where grml's main files should be copied to"""
1351
1352     global GRML_DEFAULT
1353     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1354     if options.dryrun:
1355         return 0
1356     elif not options.bootloaderonly:
1357         logging.info("Copying files. This might take a while....")
1358         try:
1359             copy_system_files(grml_flavour, iso_mount, target)
1360             copy_grml_files(iso_mount, target)
1361         except CriticalException, error:
1362             logging.critical("Execution failed: %s", error)
1363             sys.exit(1)
1364
1365     if not options.skipaddons:
1366         if not search_file('addons', iso_mount):
1367             logging.info("Could not find addons, therefore not installing.")
1368         else:
1369             copy_addons(iso_mount, target)
1370
1371     if not options.copyonly:
1372         copy_bootloader_files(iso_mount, target, grml_flavour)
1373
1374         if not options.dryrun:
1375             handle_bootloader_config(grml_flavour, device, target)
1376
1377     # make sure we sync filesystems before returning
1378     proc = subprocess.Popen(["sync"])
1379     proc.wait()
1380
1381
1382 def get_flavour(flavour_str):
1383     """Returns the flavour of a grml version string
1384     """
1385     return re.match(r'[\w-]*', flavour_str).group()
1386
1387 def identify_grml_flavour(mountpath):
1388     """Get name of grml flavour
1389
1390     @mountpath: path where the grml ISO is mounted to
1391     @return: name of grml-flavour"""
1392
1393     version_file = search_file('grml-version', mountpath)
1394
1395     if version_file == "":
1396         logging.critical("Error: could not find grml-version file.")
1397         raise
1398
1399     flavours = []
1400     tmpfile = None
1401     try:
1402         tmpfile = open(version_file, 'r')
1403         for line in tmpfile.readlines():
1404             flavours.append(get_flavour(line))
1405     except TypeError, e:
1406         raise
1407     except Exception, e:
1408         logging.critical("Unexpected error: %s", e)
1409         raise
1410     finally:
1411         if tmpfile:
1412             tmpfile.close()
1413
1414     return flavours
1415
1416
1417 def modify_grub_config(filename):
1418     """Adjust bootoptions for a grub file
1419
1420     @filename: filename to modify
1421     """
1422     if options.removeoption:
1423         regexe = []
1424         for regex in options.removeoption:
1425             regexe.append(re.compile(r'%s' % regex))
1426
1427         option_re = re.compile(r'(.*/boot/release/.*linux26.*)')
1428
1429         for line in fileinput.input(filename, inplace=1):
1430             if regexe and option_re.search(line):
1431                 for regex in regexe:
1432                     line = regex.sub(' ', line)
1433
1434             sys.stdout.write(line)
1435
1436         fileinput.close()
1437
1438 def handle_grub1_config(grml_flavour, install_partition, grub_target, bootopt):
1439     """Main handler for generating grub1 configuration
1440
1441     @grml_flavour: name of grml flavour the configuration should be generated for
1442     @install_partition: partition number for use in (hd0,X)
1443     @grub_target: path of grub's configuration files
1444     @bootoptions: additional bootoptions that should be used by default"""
1445
1446     # grub1 config
1447     grub1_cfg = grub_target + 'menu.lst'
1448     logging.debug("Creating grub1 configuration file (menu.lst)")
1449
1450     # install main configuration only *once*, no matter how many ISOs we have:
1451     if os.path.isfile(grub1_cfg):
1452         string = open(grub1_cfg).readline()
1453         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1454         if not re.match(main_identifier, string):
1455             grub1_config_file = open(grub1_cfg, 'w')
1456             grub1_config_file.write(generate_main_grub1_config(grml_flavour, install_partition, bootopt))
1457             grub1_config_file.close()
1458     else:
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
1463     grub_flavour_config = True
1464     if os.path.isfile(grub1_cfg):
1465         string = open(grub1_cfg).readlines()
1466         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1467         for line in string:
1468             if flavour.match(line):
1469                 grub_flavour_config = False
1470
1471     if grub_flavour_config:
1472         grub1_config_file = open(grub1_cfg, 'a')
1473         grub1_config_file.write(generate_flavour_specific_grub1_config(grml_flavour, install_partition, bootopt))
1474         grub1_config_file.close()
1475
1476     modify_grub_config(grub1_cfg)
1477
1478     # make sure grub.conf isn't a symlink but a plain file instead,
1479     # otherwise it will break on FAT16 filesystems
1480     # this works around grub-install of (at least) Fedora 10
1481     if os.path.isfile(grub1_cfg):
1482         grubconf = grub_target + 'grub.conf'
1483         if not os.path.islink(grubconf):
1484             import shutil
1485             shutil.copyfile(grub1_cfg, grub_target + 'grub.conf')
1486
1487 def handle_grub2_config(grml_flavour, grub_target, bootopt):
1488     """Main handler for generating grub2 configuration
1489
1490     @grml_flavour: name of grml flavour the configuration should be generated for
1491     @grub_target: path of grub's configuration files
1492     @bootoptions: additional bootoptions that should be used by default"""
1493
1494     # grub2 config
1495     grub2_cfg = grub_target + 'grub.cfg'
1496     logging.debug("Creating grub2 configuration file (grub.lst)")
1497
1498     global GRML_DEFAULT
1499
1500     # install main configuration only *once*, no matter how many ISOs we have:
1501     grub_flavour_is_default = False
1502     if os.path.isfile(grub2_cfg):
1503         string = open(grub2_cfg).readline()
1504         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1505         if not re.match(main_identifier, string):
1506             grub2_config_file = open(grub2_cfg, 'w')
1507             GRML_DEFAULT = grml_flavour
1508             grub_flavour_is_default = True
1509             grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1510             grub2_config_file.close()
1511     else:
1512         grub2_config_file = open(grub2_cfg, 'w')
1513         GRML_DEFAULT = grml_flavour
1514         grub_flavour_is_default = True
1515         grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1516         grub2_config_file.close()
1517
1518     # install flavour specific configuration only *once* as well
1519     grub_flavour_config = True
1520     if os.path.isfile(grub2_cfg):
1521         string = open(grub2_cfg).readlines()
1522         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1523         for line in string:
1524             if flavour.match(line):
1525                 grub_flavour_config = False
1526
1527     if grub_flavour_config:
1528         grub2_config_file = open(grub2_cfg, 'a')
1529         # display only if the grml flavour isn't the default
1530         if not grub_flavour_is_default:
1531             GRML_FLAVOURS.add(grml_flavour)
1532         grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, bootopt))
1533         grub2_config_file.close()
1534
1535     modify_grub_config(grub2_cfg)
1536
1537
1538 def get_bootoptions(grml_flavour):
1539     """Returns bootoptions for specific flavour
1540
1541     @grml_flavour: name of the grml_flavour
1542     """
1543     # do NOT write "None" in kernel cmdline
1544     if options.bootoptions is None:
1545         bootopt = ""
1546     else:
1547         bootopt = options.bootoptions
1548     bootopt = bootopt.replace("%flavour", grml_flavour)
1549     return bootopt
1550
1551
1552 def handle_grub_config(grml_flavour, device, target):
1553     """Main handler for generating grub (v1 and v2) configuration
1554
1555     @grml_flavour: name of grml flavour the configuration should be generated for
1556     @device: device/partition where grub should be installed to
1557     @target: path of grub's configuration files"""
1558
1559     logging.debug("Generating grub configuration")
1560
1561     grub_target = target + '/boot/grub/'
1562
1563     if os.path.isdir(device):
1564         install_grub1_partition = None
1565     else:
1566         if device[-1:].isdigit():
1567             install_grub1_partition = int(device[-1:]) - 1
1568         else:
1569             raise CriticalException("error validating partition schema (raw device?)")
1570
1571
1572     bootopt = get_bootoptions(grml_flavour)
1573
1574     # write menu.lst
1575     handle_grub1_config(grml_flavour, install_grub1_partition, grub_target, bootopt)
1576     # write grub.cfg
1577     handle_grub2_config(grml_flavour, grub_target, bootopt)
1578
1579
1580 def initial_syslinux_config(target):
1581     """Generates intial syslinux configuration
1582
1583     @target path of syslinux's configuration files"""
1584
1585     target = target + "/"
1586     filename = target + "grmlmain.cfg"
1587     if os.path.isfile(target + "grmlmain.cfg"):
1588         return
1589     data = open(filename, "w")
1590     data.write(generate_main_syslinux_config())
1591     data.close()
1592
1593     filename = target + "hiddens.cfg"
1594     data = open(filename, "w")
1595     data.write("include hidden.cfg\n")
1596     data.close()
1597
1598 def add_entry_if_not_present(filename, entry):
1599     """Write entry into filename if entry is not already in the file
1600
1601     @filanme: name of the file
1602     @entry: data to write to the file
1603     """
1604     data = open(filename, "a+")
1605     for line in data:
1606         if line == entry:
1607             break
1608     else:
1609         data.write(entry)
1610
1611     data.close()
1612
1613 def get_flavour_filename(flavour):
1614     """Generate a iso9960 save filename out of the specified flavour
1615
1616     @flavour: grml flavour
1617     """
1618     return flavour.replace('-', '_')
1619
1620 def adjust_syslinux_bootoptions(src, flavour):
1621     """Adjust existing bootoptions of specified syslinux config to
1622     grml2usb specific ones, e.g. change the location of the kernel...
1623
1624     @src: config file to alter
1625     @flavour: grml flavour
1626     """
1627
1628     append_re = re.compile("^(\s*append.*/boot/release.*)$", re.I)
1629     boot_re = re.compile("/boot/([a-zA-Z0-9_]+/)+([a-zA-Z0-9._]+)")
1630     # flavour_re = re.compile("(label.*)(grml\w+)")
1631     default_re = re.compile("(default.cfg)")
1632     bootid_re = re.compile("bootid=[\w_-]+")
1633     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1634
1635     bootopt = get_bootoptions(flavour)
1636
1637     regexe = []
1638     option_re = None
1639     if options.removeoption:
1640         option_re = re.compile(r'/boot/release/.*/initrd.gz')
1641
1642         for regex in options.removeoption:
1643             regexe.append(re.compile(r'%s' % regex))
1644
1645     for line in fileinput.input(src, inplace=1):
1646         line = boot_re.sub(r'/boot/release/%s/\2 ' % flavour.replace('-', ''), line)
1647         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1648         line = default_re.sub(r'%s-\1' % flavour, line)
1649         line = bootid_re.sub('', line)
1650         line = live_media_path_re.sub('', line)
1651         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1652         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1653         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1654         if option_re and option_re.search(line):
1655             for regex in regexe:
1656                 line = regex.sub(' ', line)
1657         sys.stdout.write(line)
1658     fileinput.close()
1659
1660 def adjust_labels(src, replacement):
1661     """Adjust the specified labels in the syslinux config file src with
1662     specified replacement
1663     """
1664     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1665     for line in fileinput.input(src, inplace=1):
1666         line = label_re.sub(replacement, line)
1667         sys.stdout.write(line)
1668     fileinput.close()
1669
1670
1671 def add_syslinux_entry(filename, grml_flavour):
1672     """Add includes for a specific grml_flavour to the specified filename
1673
1674     @filename: syslinux config file
1675     @grml_flavour: grml flavour to add
1676     """
1677
1678     entry_filename = "option_%s.cfg" % grml_flavour
1679     entry = "include %s\n" % entry_filename
1680
1681     add_entry_if_not_present(filename, entry)
1682     path = os.path.dirname(filename)
1683
1684     data = open(path + "/" + entry_filename, "w")
1685     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1686     data.close()
1687
1688 def modify_filenames(grml_flavour, target, filenames):
1689     """Replace the standarf filenames with the new ones
1690
1691     @grml_flavour: grml-flavour strin
1692     @target: directory where the files are located
1693     @filenames: list of filenames to alter
1694     """
1695     grml_filename = grml_flavour.replace('-', '_')
1696     for filename in filenames:
1697         old_filename = "%s/%s" % (target, filename)
1698         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1699         os.rename(old_filename, new_filename)
1700
1701
1702 def remove_default_entry(filename):
1703     """Remove the default entry from specified syslinux file
1704
1705     @filename: syslinux config file
1706     """
1707     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1708     for line in fileinput.input(filename, inplace=1):
1709         if default_re.match(line):
1710             continue
1711         sys.stdout.write(line)
1712     fileinput.close()
1713
1714
1715 def handle_syslinux_config(grml_flavour, target):
1716     """Main handler for generating syslinux configuration
1717
1718     @grml_flavour: name of grml flavour the configuration should be generated for
1719     @target: path of syslinux's configuration files"""
1720
1721     logging.debug("Generating syslinux configuration")
1722     syslinux_target = target + '/boot/syslinux/'
1723     # should be present via  copy_bootloader_files(), but make sure it exits:
1724     execute(mkdir, syslinux_target)
1725     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1726
1727
1728     # install main configuration only *once*, no matter how many ISOs we have:
1729     syslinux_config_file = open(syslinux_cfg, 'w')
1730     syslinux_config_file.write("TIMEOUT 300\n")
1731     syslinux_config_file.write("include vesamenu.cfg\n")
1732     syslinux_config_file.close()
1733
1734     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1735     prompt_name.write('menu label S^yslinux prompt\n')
1736     prompt_name.close()
1737
1738     initial_syslinux_config(syslinux_target)
1739     flavour_filename = grml_flavour.replace('-', '_')
1740
1741     if search_file('default.cfg', syslinux_target):
1742         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1743
1744     filename = search_file("new_hidden.cfg", syslinux_target)
1745
1746
1747     # process hidden file
1748     if not search_file("hidden.cfg", syslinux_target):
1749         new_hidden = syslinux_target + "hidden.cfg"
1750         os.rename(filename, new_hidden)
1751         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1752     else:
1753         new_hidden_file =  "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1754         os.rename(filename, new_hidden_file)
1755         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1756         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1757         entry = 'include %s_hidden.cfg\n' % flavour_filename
1758         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1759
1760
1761
1762     new_default = "%s_default.cfg" % (flavour_filename)
1763     entry = 'include %s\n' % new_default
1764     defaults_file = '%s/defaults.cfg' % syslinux_target
1765     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1766     new_grml_cfg = "%s/%s_grml.cfg" % ( syslinux_target, flavour_filename)
1767
1768     if os.path.isfile(defaults_file):
1769
1770         # remove default menu entry in menu
1771         remove_default_entry(new_default_with_path)
1772
1773         # adjust all labels for additional isos
1774         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1775         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1776
1777     # always adjust bootoptions
1778     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1779     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1780
1781     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1782
1783     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1784
1785
1786 def handle_bootloader_config(grml_flavour, device, target):
1787     """Main handler for generating bootloader's configuration
1788
1789     @grml_flavour: name of grml flavour the configuration should be generated for
1790     @device: device/partition where bootloader should be installed to
1791     @target: path of bootloader's configuration files"""
1792
1793     global UUID
1794     UUID = get_uuid(target)
1795     if options.skipsyslinuxconfig:
1796         logging.info("Skipping generation of syslinux configuration as requested.")
1797     else:
1798         try:
1799             handle_syslinux_config(grml_flavour, target)
1800         except CriticalException, error:
1801             logging.critical("Fatal: %s", error)
1802             sys.exit(1)
1803
1804     if options.skipgrubconfig:
1805         logging.info("Skipping generation of grub configuration as requested.")
1806     else:
1807         try:
1808             handle_grub_config(grml_flavour, device, target)
1809         except CriticalException, error:
1810             logging.critical("Fatal: %s", error)
1811             sys.exit(1)
1812
1813
1814
1815 def install(image, device):
1816     """Install a grml image to the specified device
1817
1818     @image: directory or is file
1819     @device: partition or directory to install the device
1820     """
1821     iso_mountpoint = image
1822     remove_image_mountpoint = False
1823     if os.path.isdir(image):
1824         logging.info("Using %s as install base", image)
1825     else:
1826         logging.info("Using ISO %s", image)
1827         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1828         register_tmpfile(iso_mountpoint)
1829         remove_image_mountpoint = True
1830         try:
1831             mount(image, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
1832         except CriticalException, error:
1833             logging.critical("Fatal: %s", error)
1834             sys.exit(1)
1835
1836     try:
1837         install_grml(iso_mountpoint, device)
1838     finally:
1839         if remove_image_mountpoint:
1840             try:
1841                 remove_mountpoint(iso_mountpoint)
1842             except CriticalException, error:
1843                 logging.critical("Fatal: %s", error)
1844                 cleanup()
1845
1846
1847
1848 def install_grml(mountpoint, device):
1849     """Main logic for copying files of the currently running grml system.
1850
1851     @mountpoin: directory where currently running live system resides (usually /live/image)
1852     @device: partition where the specified ISO should be installed to"""
1853
1854     device_mountpoint = device
1855     if os.path.isdir(device):
1856         logging.info("Specified device is not a directory, therefore not mounting.")
1857         remove_device_mountpoint = False
1858     else:
1859         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1860         register_tmpfile(device_mountpoint)
1861         remove_device_mountpoint = True
1862         try:
1863             check_for_fat(device)
1864             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1865         except CriticalException, error:
1866             try:
1867                 mount(device, device_mountpoint, "")
1868             except CriticalException, error:
1869                 logging.critical("Fatal: %s", error)
1870                 raise
1871     global GRML_FLAVOURS
1872     try:
1873         grml_flavours = identify_grml_flavour(mountpoint)
1874         for flavour in set(grml_flavours):
1875             logging.info("Identified grml flavour \"%s\".", flavour)
1876             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1877             GRML_FLAVOURS.add(flavour)
1878     finally:
1879         if remove_device_mountpoint:
1880             remove_mountpoint(device_mountpoint)
1881
1882 def remove_mountpoint(mountpoint):
1883     """remove a registred mountpoint
1884     """
1885
1886     try:
1887         unmount(mountpoint, "")
1888         if os.path.isdir(mountpoint):
1889             os.rmdir(mountpoint)
1890             unregister_tmpfile(mountpoint)
1891     except CriticalException, error:
1892         logging.critical("Fatal: %s", error)
1893         cleanup()
1894
1895 def handle_mbr(device):
1896     """Main handler for installing master boot record (MBR)
1897
1898     @device: device where the MBR should be installed to"""
1899
1900     if options.dryrun:
1901         logging.info("Would install MBR")
1902         return 0
1903
1904     if device[-1:].isdigit():
1905         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1906         partition_number = int(device[-1:]) - 1
1907         skip_install_mir_mbr = False
1908
1909     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1910     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1911     if mbr_device == "/dev/loop":
1912         mbr_device = device
1913         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1914         skip_install_mir_mbr = True
1915
1916     try:
1917         if options.syslinuxmbr:
1918             handle_syslinux_mbr(mbr_device)
1919         elif not skip_install_mir_mbr:
1920             if options.mbrmenu:
1921                 install_mir_mbr(GRML2USB_BASE + '/mbr/mbrmgr', mbr_device, partition_number, True)
1922             else:
1923                 install_mir_mbr(GRML2USB_BASE + '/mbr/mbrldr', mbr_device, partition_number, False)
1924     except IOError, error:
1925         logging.critical("Execution failed: %s", error)
1926         sys.exit(1)
1927     except Exception, error:
1928         logging.critical("Execution failed: %s", error)
1929         sys.exit(1)
1930
1931
1932 def handle_vfat(device):
1933     """Check for FAT specific settings and options
1934
1935     @device: device that should checked / formated"""
1936
1937     # make sure we have mkfs.vfat available
1938     if options.fat16:
1939         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1940             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1941             logging.critical('Please make sure to install dosfstools.')
1942             sys.exit(1)
1943
1944         if options.force:
1945             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1946         else:
1947             # make sure the user is aware of what he is doing
1948             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1949             if f == "y" or f == "Y":
1950                 logging.info("Note: you can skip this question using the option --force")
1951             else:
1952                 sys.exit(1)
1953         try:
1954             mkfs_fat16(device)
1955         except CriticalException, error:
1956             logging.critical("Execution failed: %s", error)
1957             sys.exit(1)
1958
1959     # check for vfat filesystem
1960     if device is not None and not os.path.isdir(device) and options.syslinux:
1961         try:
1962             check_for_fat(device)
1963         except CriticalException, error:
1964             logging.critical("Execution failed: %s", error)
1965             sys.exit(1)
1966
1967     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1968         print "Warning: the specified device %s does not look like a removable usb device." % device
1969         f = raw_input("Do you really want to continue? y/N ")
1970         if f == "y" or f == "Y":
1971             pass
1972         else:
1973             sys.exit(1)
1974
1975
1976 def handle_compat_warning(device):
1977     """Backwards compatible checks
1978
1979     @device: device that should be checked"""
1980
1981     # make sure we can replace old grml2usb script and warn user when using old way of life:
1982     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1983         print "Warning: the semantics of grml2usb has changed."
1984         print "Instead of using grml2usb /path/to/iso %s you might" % device
1985         print "want to use grml2usb /path/to/iso /dev/... instead."
1986         print "Please check out the grml2usb manpage for details."
1987         f = raw_input("Do you really want to continue? y/N ")
1988         if f == "y" or f == "Y":
1989             pass
1990         else:
1991             sys.exit(1)
1992
1993
1994 def handle_logging():
1995     """Log handling and configuration"""
1996
1997     if options.verbose and options.quiet:
1998         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1999
2000     if options.verbose:
2001         FORMAT = "Debug: %(asctime)-15s %(message)s"
2002         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
2003     elif options.quiet:
2004         FORMAT = "Critical: %(message)s"
2005         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
2006     else:
2007         FORMAT = "%(message)s"
2008         logging.basicConfig(level=logging.INFO, format=FORMAT)
2009
2010
2011 def handle_bootloader(device):
2012     """wrapper for installing bootloader
2013
2014     @device: device where bootloader should be installed to"""
2015
2016     # Install bootloader only if not using the --copy-only option
2017     if options.copyonly:
2018         logging.info("Not installing bootloader and its files as requested via option copyonly.")
2019     elif os.path.isdir(device):
2020         logging.info("Not installing bootloader as %s is a directory.", device)
2021     else:
2022         install_bootloader(device)
2023
2024
2025 def check_options(opts):
2026     """Check compability of provided user opts
2027
2028     @opts option dict from OptionParser
2029     """
2030     if opts.grubmbr and not opts.grub:
2031         logging.critical("Error: --grub-mbr requires --grub option.")
2032         sys.exit(1)
2033
2034
2035 def main():
2036     """Main function [make pylint happy :)]"""
2037
2038     if options.version:
2039         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
2040         sys.exit(0)
2041
2042     if len(args) < 2:
2043         parser.error("invalid usage")
2044
2045     # log handling
2046     handle_logging()
2047
2048     # make sure we have the appropriate permissions
2049     check_uid_root()
2050
2051     check_options(options)
2052
2053     logging.info("Executing grml2usb version %s", PROG_VERSION)
2054
2055     if options.dryrun:
2056         logging.info("Running in simulation mode as requested via option dry-run.")
2057
2058     # specified arguments
2059     device = args[len(args) - 1]
2060     isos = args[0:len(args) - 1]
2061
2062     if not os.path.isdir(device):
2063         if device[-1:].isdigit():
2064             if int(device[-1:]) > 4 or device[-2:].isdigit():
2065                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
2066                 sys.exit(1)
2067         elif os.path.exists(device):
2068             logging.critical("Fatal: installation on raw device not supported. (BIOS won't support it.)")
2069             sys.exit(1)
2070
2071     if options.grub:
2072         if not which("grub-install"):
2073             logging.critical("Fatal: grub-install not available (please install the "
2074                              + "grub package or drop the --grub option)")
2075             sys.exit(1)
2076
2077     if options.syslinux:
2078         if not which("syslinux"):
2079             logging.critical("Fatal: syslinux not available (please install the "
2080                              + "syslinux package or use the --grub option)")
2081             sys.exit(1)
2082
2083     if not which("rsync"):
2084         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2085         sys.exit(1)
2086
2087     # provide upgrade path
2088     handle_compat_warning(device)
2089
2090     # check for vfat partition
2091     handle_vfat(device)
2092
2093     # main operation (like installing files)
2094     for iso in isos:
2095         install(iso, device)
2096
2097     # install mbr
2098     if not options.skipmbr and not os.path.isdir(device):
2099         handle_mbr(device)
2100
2101     handle_bootloader(device)
2102
2103     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
2104
2105     for flavour in GRML_FLAVOURS:
2106         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
2107
2108     # finally be politely :)
2109     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
2110
2111
2112 if __name__ == "__main__":
2113     try:
2114         main()
2115     except KeyboardInterrupt:
2116         logging.info("Received KeyboardInterrupt")
2117         cleanup()
2118
2119 ## END OF FILE #################################################################
2120 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8