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