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