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