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