8ffe71ff22e763243661fa2de50820ee9f62b02c
[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.info("Installing grub as bootloader")
586                 logging.debug("grub-install --recheck --no-floppy --root-directory=%s %s",
587                     device_mountpoint, grub_device)
588                 proc = subprocess.Popen(["grub-install", "--recheck", "--no-floppy",
589                     "--root-directory=%s" % device_mountpoint, grub_device], stdout=file(os.devnull, "r+"))
590                 proc.wait()
591                 if proc.returncode != 0:
592                     # raise Exception("error executing grub-install")
593                     logging.critical("Fatal: error executing grub-install (please check the grml2usb FAQ or drop the --grub option)")
594                     logging.critical("Note:  if using grub2 consider using the --grub-mbr option because grub2's PBR feature is broken.")
595                     cleanup()
596                     sys.exit(1)
597             except CriticalException, error:
598                 logging.critical("Fatal: %s", error)
599                 cleanup()
600                 sys.exit(1)
601
602         finally:
603             unmount(device_mountpoint, "")
604             os.rmdir(device_mountpoint)
605             unregister_tmpfile(device_mountpoint)
606
607
608 def install_syslinux(device):
609     """Install syslinux on specified device.
610
611     @device: partition where syslinux should be installed to"""
612
613     if options.dryrun:
614         logging.info("Would install syslinux as bootloader on %s", device)
615         return 0
616
617     # syslinux -d boot/isolinux /dev/sdb1
618     logging.info("Installing syslinux as bootloader")
619     logging.debug("syslinux -d boot/syslinux %s", device)
620     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
621     proc.wait()
622     if proc.returncode != 0:
623         raise CriticalException("Error executing syslinux (either try --fat16 or use grub?)")
624
625
626 def install_bootloader(device):
627     """Install bootloader on specified device.
628
629     @device: partition where bootloader should be installed to"""
630
631     # by default we use grub, so install syslinux only on request
632     if options.syslinux:
633         logging.info("Note: the --syslinux option is deprecated as syslinux is grml2usb's default. Continuing anyway.")
634
635     if options.grub:
636         if not which("grub-install"):
637             logging.critical("Fatal: grub-install not available (please install the grub package or use the --syslinux option)")
638             cleanup()
639             sys.exit(1)
640         else:
641             try:
642                 install_grub(device)
643             except CriticalException, error:
644                 logging.critical("Fatal: %s", error)
645                 cleanup()
646                 sys.exit(1)
647     else:
648         try:
649             install_syslinux(device)
650         except CriticalException, error:
651             logging.critical("Fatal: %s", error)
652             cleanup()
653             sys.exit(1)
654
655
656 def execute_lilo(lilo, device):
657     """execute lilo for activating the partitions in the MBR
658
659     @lilo: path of lilo executable
660     @device: device where lilo should be executed on"""
661
662     # to support -A for extended partitions:
663     logging.info("Activating partitions in MBR via lilo")
664     logging.debug("%s -S /dev/null -M %s ext", lilo, device)
665     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
666     proc.wait()
667     if proc.returncode != 0:
668         raise Exception("error executing lilo")
669
670     # activate partition:
671     logging.debug("%s -S /dev/null -A %s 1", lilo, device)
672     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
673     proc.wait()
674     if proc.returncode != 0:
675         raise Exception("error executing lilo")
676
677
678 def install_syslinux_mbr(device):
679     """install syslinux's master boot record (MBR) on the specified device
680
681     @device: device where MBR of syslinux should be installed to"""
682
683     # make sure we have syslinux available
684     if not which("syslinux") and not options.copyonly:
685         raise Exception("syslinux not available (either install it or consider using the --grub option)")
686
687     # lilo's mbr is broken, use the one from syslinux instead:
688     if not os.path.isfile("/usr/lib/syslinux/mbr.bin"):
689         raise Exception("/usr/lib/syslinux/mbr.bin can not be read")
690
691     logging.info("Installing syslinux MBR")
692     logging.debug("cat /usr/lib/syslinux/mbr.bin > %s", device)
693     try:
694         # TODO -> use Popen instead?
695         retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
696         if retcode < 0:
697             logging.critical("Error copying MBR to device (%s)", retcode)
698     except OSError, error:
699         logging.critical("Execution failed: %s", error)
700
701
702 def install_mir_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
703     """install 'mbr' master boot record (MBR) on a device
704
705     Retrieve the partition table from "device", install an MBR from the
706     "mbrtemplate" file, set the "partition" (0..3) active, and install the
707     result back to "device".
708
709     @mbrtemplate: default MBR file
710
711     @device: name of a file assumed to be a hard disc (or USB stick) image, or
712     something like "/dev/sdb"
713
714     @partition: must be a number between 0 and 3, inclusive
715
716     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
717     ismirbsdmbr) bytes.
718
719     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
720     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
721     false then leave the mirbsdmbr specific flag set to FFh, set all
722     active flags to 0 and set the active flag of the partition to 80h.  Note:
723     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
724     the active flag."""
725
726     logging.info("Installing default MBR")
727
728     if not os.path.isfile(mbrtemplate):
729         logging.critical("Error: %s can not be read.", mbrtemplate)
730         raise CriticalException("Error installing MBR (either try --syslinux-mbr or install missing file?)")
731
732     if (partition < 0) or (partition > 3):
733         raise ValueError("partition must be between 0 and 3")
734
735     if ismirbsdmbr:
736         nmbrbytes = 439
737     else:
738         nmbrbytes = 440
739
740     tmpf = tempfile.NamedTemporaryFile()
741
742     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
743     proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"], stderr=file(os.devnull, "r+"))
744     proc.wait()
745     if proc.returncode != 0:
746         raise Exception("error executing dd (first run)")
747
748     logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc", mbrtemplate,
749         tmpf.name, nmbrbytes)
750     proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes,
751         "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
752     proc.wait()
753     if proc.returncode != 0:
754         raise Exception("error executing dd (second run)")
755
756     mbrcode = tmpf.file.read(512)
757     if len(mbrcode) < 512:
758         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
759
760     if ismirbsdmbr:
761         mbrcode = mbrcode[0:439] + chr(partition) + \
762           mbrcode[440:510] + "\x55\xAA"
763     else:
764         actives = ["\x00", "\x00", "\x00", "\x00"]
765         actives[partition] = "\x80"
766         mbrcode = mbrcode[0:446] + actives[0] + \
767           mbrcode[447:462] + actives[1] + \
768           mbrcode[463:478] + actives[2] + \
769           mbrcode[479:494] + actives[3] + \
770           mbrcode[495:510] + "\x55\xAA"
771
772     tmpf.file.seek(0)
773     tmpf.file.truncate()
774     tmpf.file.write(mbrcode)
775     tmpf.file.close()
776
777     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device)
778     proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % device, "bs=512", "count=1",
779                             "conv=notrunc"], stderr=file(os.devnull, "r+"))
780     proc.wait()
781     if proc.returncode != 0:
782         raise Exception("error executing dd (third run)")
783     del tmpf
784
785
786 def handle_syslinux_mbr(device):
787     """Install syslinux master boot record on given device
788
789     @device: device where MBR should be installed to"""
790
791     if not is_writeable(device):
792         raise IOError("device not writeable for user")
793
794     # try to use system's lilo
795     if which("lilo"):
796         lilo = which("lilo")
797     else:
798         # otherwise fall back to our static version
799         from platform import architecture
800         if architecture()[0] == '64bit':
801             lilo = GRML2USB_BASE + '/lilo/lilo.static.amd64'
802         else:
803             lilo = GRML2USB_BASE + '/lilo/lilo.static.i386'
804     # finally prefer a specified lilo executable
805     if options.lilobin:
806         lilo = options.lilobin
807
808     if not is_exe(lilo):
809         raise Exception("lilo executable can not be execute")
810
811     if options.dryrun:
812         logging.info("Would install MBR running lilo and using syslinux.")
813         return 0
814
815     execute_lilo(lilo, device)
816     install_syslinux_mbr(device)
817
818
819 def is_writeable(device):
820     """Check if the device is writeable for the current user
821
822     @device: partition where bootloader should be installed to"""
823
824     if not device:
825         return False
826         #raise Exception("no device for checking write permissions")
827
828     if not os.path.exists(device):
829         return False
830
831     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
832
833
834 def mount(source, target, mount_options):
835     """Mount specified source on given target
836
837     @source: name of device/ISO that should be mounted
838     @target: directory where the ISO should be mounted to
839     @options: mount specific options"""
840
841     # note: options.dryrun does not work here, as we have to
842     # locate files and identify the grml flavour
843
844     for x in file('/proc/mounts').readlines():
845         if x.startswith(source):
846             raise CriticalException("Error executing mount: %s already mounted - please unmount before invoking grml2usb" % source)
847
848     if os.path.isdir(source):
849         logging.debug("Source %s is not a device, therefore not mounting.", source)
850         return 0
851
852     logging.debug("mount %s %s %s", mount_options, source, target)
853     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
854     proc.wait()
855     if proc.returncode != 0:
856         raise CriticalException("Error executing mount (no filesystem on the partition?)")
857     else:
858         logging.debug("register_mountpoint(%s)", target)
859         register_mountpoint(target)
860
861
862 def unmount(target, unmount_options):
863     """Unmount specified target
864
865     @target: target where something is mounted on and which should be unmounted
866     @options: options for umount command"""
867
868     # make sure we unmount only already mounted targets
869     target_unmount = False
870     mounts = open('/proc/mounts').readlines()
871     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
872     for line in mounts:
873         if re.match(mountstring, line):
874             target_unmount = True
875
876     if not target_unmount:
877         logging.debug("%s not mounted anymore", target)
878     else:
879         logging.debug("umount %s %s", list(unmount_options), target)
880         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
881         proc.wait()
882         if proc.returncode != 0:
883             raise Exception("Error executing umount")
884         else:
885             logging.debug("unregister_mountpoint(%s)", target)
886             unregister_mountpoint(target)
887
888
889 def check_for_usbdevice(device):
890     """Check whether the specified device is a removable USB device
891
892     @device: device name, like /dev/sda1 or /dev/sda
893     """
894
895     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
896     # newer systems:
897     usbdev = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
898     if not os.path.isfile(usbdev):
899         # Ubuntu with kernel 2.6.24 for example:
900         usbdev = os.path.realpath('/sys/block/' + usbdevice + '/removable')
901
902     if os.path.isfile(usbdev):
903         is_usb = open(usbdev).readline()
904         if is_usb.find("1"):
905             return 0
906
907     return 1
908
909
910 def check_for_fat(partition):
911     """Check whether specified partition is a valid VFAT/FAT16 filesystem
912
913     @partition: device name of partition"""
914
915     try:
916         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
917                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
918         filesystem = udev_info.communicate()[0].rstrip()
919
920         if udev_info.returncode == 2:
921             raise CriticalException("Failed to read device %s"
922                                     " (wrong UID/permissions or device/directory not present?)" % partition)
923
924         if options.syslinux and filesystem != "vfat":
925             raise CriticalException("Partition %s does not contain a FAT16 filesystem. (Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
926
927     except OSError:
928         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
929
930
931 def mkdir(directory):
932     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
933
934     # just silently pass as it's just fine it the directory exists
935     if not os.path.isdir(directory):
936         try:
937             os.makedirs(directory)
938         # pylint: disable-msg=W0704
939         except OSError:
940             pass
941
942
943 def exec_rsync(source, target):
944     """Simple wrapper around rsync to install files
945
946     @source: source file/directory
947     @target: target file/directory"""
948     logging.debug("Source: %s / Target: %s", source, target)
949     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
950     proc.wait()
951     if proc.returncode == 12:
952         logging.critical("Fatal: No space left on device")
953         cleanup()
954         sys.exit(1)
955
956     if proc.returncode != 0:
957         logging.critical("Fatal: could not install %s", source)
958         cleanup()
959         sys.exit(1)
960
961
962 def copy_system_files(grml_flavour, iso_mount, target):
963     """copy grml's main files (like squashfs, kernel and initrd) to a given target
964
965     @grml_flavour: name of grml flavour the configuration should be generated for
966     @iso_mount: path where a grml ISO is mounted on
967     @target: path where grml's main files should be copied to"""
968
969     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
970     if squashfs is None:
971         logging.critical("Fatal: squashfs file not found")
972         raise CriticalException("error locating squashfs file")
973     else:
974         squashfs_target = target + '/live/' + grml_flavour + '/'
975         execute(mkdir, squashfs_target)
976     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
977
978     filesystem_module = search_file('filesystem.module', iso_mount)
979     if filesystem_module is None:
980         logging.critical("Fatal: filesystem.module not found")
981         raise CriticalException("error locating filesystem.module file")
982     else:
983         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
984
985     release_target = target + '/boot/release/' + grml_flavour
986     execute(mkdir, release_target)
987
988     kernel = search_file('linux26', iso_mount)
989     if kernel is None:
990         logging.critical("Fatal kernel not found")
991         raise CriticalException("error locating kernel file")
992     else:
993         exec_rsync(kernel, release_target + '/linux26')
994
995     initrd = search_file('initrd.gz', iso_mount)
996     if initrd is None:
997         logging.critical("Fatal: initrd not found")
998         raise CriticalException("error locating initrd file")
999     else:
1000         exec_rsync(initrd, release_target + '/initrd.gz')
1001
1002
1003 def copy_grml_files(iso_mount, target):
1004     """copy some minor grml files to a given target
1005
1006     @iso_mount: path where a grml ISO is mounted on
1007     @target: path where grml's main files should be copied to"""
1008
1009     grml_target = target + '/grml/'
1010     execute(mkdir, grml_target)
1011
1012     for myfile in 'grml-cheatcodes.txt', 'grml-version', 'LICENSE.txt', 'md5sums', 'README.txt':
1013         grml_file = search_file(myfile, iso_mount)
1014         if grml_file is None:
1015             logging.warn("Warning: myfile %s could not be found - can not install it", myfile)
1016         else:
1017             exec_rsync(grml_file, grml_target + myfile)
1018
1019     grml_web_target = grml_target + '/web/'
1020     execute(mkdir, grml_web_target)
1021
1022     for myfile in 'index.html', 'style.css':
1023         grml_file = search_file(myfile, iso_mount)
1024         if grml_file is None:
1025             logging.warn("Warning: myfile %s could not be found - can not install it")
1026         else:
1027             exec_rsync(grml_file, grml_web_target + myfile)
1028
1029     grml_webimg_target = grml_web_target + '/images/'
1030     execute(mkdir, grml_webimg_target)
1031
1032     for myfile in 'button.png', 'favicon.png', 'linux.jpg', 'logo.png':
1033         grml_file = search_file(myfile, iso_mount)
1034         if grml_file is None:
1035             logging.warn("Warning: myfile %s could not be found - can not install it")
1036         else:
1037             exec_rsync(grml_file, grml_webimg_target + myfile)
1038
1039
1040 def copy_addons(iso_mount, target):
1041     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1042
1043     @iso_mount: path where a grml ISO is mounted on
1044     @target: path where grml's main files should be copied to"""
1045
1046     addons = target + '/boot/addons/'
1047     execute(mkdir, addons)
1048
1049     # grub all-in-one image
1050     allinoneimg = search_file('allinone.img', iso_mount)
1051     if allinoneimg is None:
1052         logging.warn("Warning: allinone.img not found (that's fine if you don't need it)")
1053     else:
1054         exec_rsync(allinoneimg, addons + 'allinone.img')
1055
1056     # bsd imag
1057     bsdimg = search_file('bsd4grml', iso_mount)
1058     if bsdimg is None:
1059         logging.warn("Warning: bsd4grml not found (that's fine if you don't need it)")
1060     else:
1061         exec_rsync(bsdimg, addons + '/')
1062
1063     # freedos image
1064     balderimg = search_file('balder10.imz', iso_mount)
1065     if balderimg is None:
1066         logging.warn("Warning: balder10.imz not found (that's fine if you don't need it)")
1067     else:
1068         exec_rsync(balderimg, addons + 'balder10.imz')
1069
1070     # install hdt and pci.ids only when using syslinux (grub doesn't support it)
1071     if options.syslinux:
1072         # hdt (hardware detection tool) image
1073         hdtimg = search_file('hdt.c32', iso_mount)
1074         if hdtimg:
1075             exec_rsync(hdtimg, addons + '/hdt.c32')
1076
1077         # pci.ids file
1078         picids = search_file('pci.ids', iso_mount)
1079         if picids:
1080             exec_rsync(picids, addons + '/pci.ids')
1081
1082     # memdisk image
1083     memdiskimg = search_file('memdisk', iso_mount)
1084     if memdiskimg is None:
1085         logging.warn("Warning: memdisk not found (that's fine if you don't need it)")
1086     else:
1087         exec_rsync(memdiskimg, addons + 'memdisk')
1088
1089     # memtest86+ image
1090     memtestimg = search_file('memtest', iso_mount)
1091     if memtestimg is None:
1092         logging.warn("Warning: memtest not found (that's fine if you don't need it)")
1093     else:
1094         exec_rsync(memtestimg, addons + 'memtest')
1095
1096
1097 def copy_bootloader_files(iso_mount, target):
1098     """copy grml's bootloader files to a given target
1099
1100     @iso_mount: path where a grml ISO is mounted on
1101     @target: path where grml's main files should be copied to"""
1102
1103     syslinux_target = target + '/boot/syslinux/'
1104     execute(mkdir, syslinux_target)
1105
1106     logo = search_file('logo.16', iso_mount)
1107     exec_rsync(logo, syslinux_target + 'logo.16')
1108
1109     for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
1110         bootsplash = search_file(ffile, iso_mount)
1111         exec_rsync(bootsplash, syslinux_target + ffile)
1112
1113     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1114     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1115     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1116         os.unlink(syslinux_target + 'ldlinux.sys')
1117
1118     if not search_file('default.cfg', iso_mount + '/boot/isolinux/'):
1119         logging.critical("Fatal: file default.cfg could not be found.")
1120         logging.critical("Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...")
1121         logging.critical("       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1122         raise
1123
1124     for filename in 'addons.cfg', 'default.cfg', 'distri.cfg', 'hidden.cfg', \
1125                     'grml.cfg', 'grml.png', 'hd.cfg', 'isoprompt.cfg', 'options.cfg', \
1126                     'vesamenu.c32', 'vesamenu.cfg', 'grml.png':
1127         path = search_file(filename, iso_mount + '/boot/isolinux/')
1128         exec_rsync(path, syslinux_target + filename)
1129
1130     grub_target = target + '/boot/grub/'
1131     execute(mkdir, grub_target)
1132
1133     if not os.path.isfile(GRML2USB_BASE + "/grub/splash.xpm.gz"):
1134         logging.critical("Error: %s/grub/splash.xpm.gz can not be read.", GRML2USB_BASE)
1135         logging.critical("Please make sure you've installed the grml2usb (Debian) package!")
1136         raise
1137     else:
1138         exec_rsync(GRML2USB_BASE + '/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz')
1139
1140     # grml splash in grub
1141     if os.path.isfile(GRML2USB_BASE + "/grub/grml.png"):
1142         exec_rsync(GRML2USB_BASE + '/grub/grml.png', grub_target + 'grml.png')
1143
1144     # font file for graphical bootsplash in grub
1145     if os.path.isfile("/usr/share/grub/ascii.pf2"):
1146         exec_rsync('/usr/share/grub/ascii.pf2', grub_target + 'ascii.pf2')
1147
1148
1149 def install_iso_files(grml_flavour, iso_mount, device, target):
1150     """Copy files from ISO to given target
1151
1152     @grml_flavour: name of grml flavour the configuration should be generated for
1153     @iso_mount: path where a grml ISO is mounted on
1154     @device: device/partition where bootloader should be installed to
1155     @target: path where grml's main files should be copied to"""
1156
1157     # TODO => several improvements:
1158     # * make sure grml_flavour, iso_mount, target are set when the function is called, otherwise raise exception
1159     # * provide alternative search_file() if file information is stored in a config.ini file?
1160     # * catch "install: .. No space left on device" & CO
1161
1162     if options.dryrun:
1163         return 0
1164     elif not options.bootloaderonly:
1165         logging.info("Copying files. This might take a while....")
1166         try:
1167             copy_system_files(grml_flavour, iso_mount, target)
1168             copy_grml_files(iso_mount, target)
1169         except CriticalException, error:
1170             logging.critical("Execution failed: %s", error)
1171             sys.exit(1)
1172
1173     if not options.skipaddons:
1174         if grml_flavour.endswith('-small'):
1175             logging.info("Note: grml-small doesn't provide any addons, not installing them therefore.")
1176         else:
1177             copy_addons(iso_mount, target)
1178
1179     if not options.copyonly:
1180         copy_bootloader_files(iso_mount, target)
1181
1182         if not options.dryrun:
1183             handle_bootloader_config(grml_flavour, device, target)
1184
1185     # make sure we sync filesystems before returning
1186     proc = subprocess.Popen(["sync"])
1187     proc.wait()
1188
1189
1190 def uninstall_files(device):
1191     """Get rid of all grml files on specified device
1192
1193     @device: partition where grml2usb files should be removed from"""
1194
1195     # TODO - not implemented yet
1196     logging.critical("TODO: uninstalling files from %s not yet implement, sorry.", device)
1197
1198
1199 def identify_grml_flavour(mountpath):
1200     """Get name of grml flavour
1201
1202     @mountpath: path where the grml ISO is mounted to
1203     @return: name of grml-flavour"""
1204
1205     version_file = search_file('grml-version', mountpath)
1206
1207     if version_file == "":
1208         logging.critical("Error: could not find grml-version file.")
1209         raise
1210
1211     try:
1212         tmpfile = open(version_file, 'r')
1213         grml_info = tmpfile.readline()
1214         grml_flavour = re.match(r'[\w-]*', grml_info).group()
1215     except TypeError:
1216         raise
1217     except Exception, e:
1218         logging.critical("Unexpected error: %s", e)
1219         raise
1220
1221     return grml_flavour
1222
1223
1224 def handle_grub1_config(grml_flavour, install_partition, grub_target, bootopt):
1225     """Main handler for generating grub1 configuration
1226
1227     @grml_flavour: name of grml flavour the configuration should be generated for
1228     @install_partition: partition number for use in (hd0,X)
1229     @grub_target: path of grub's configuration files
1230     @bootoptions: additional bootoptions that should be used by default"""
1231
1232     # grub1 config
1233     grub1_cfg = grub_target + 'menu.lst'
1234     logging.debug("Creating grub1 configuration file (menu.lst)")
1235
1236     # install main configuration only *once*, no matter how many ISOs we have:
1237     if os.path.isfile(grub1_cfg):
1238         string = open(grub1_cfg).readline()
1239         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1240         if not re.match(main_identifier, string):
1241             grub1_config_file = open(grub1_cfg, 'w')
1242             grub1_config_file.write(generate_main_grub1_config(grml_flavour, install_partition, bootopt))
1243             grub1_config_file.close()
1244     else:
1245         grub1_config_file = open(grub1_cfg, 'w')
1246         grub1_config_file.write(generate_main_grub1_config(grml_flavour, install_partition, bootopt))
1247         grub1_config_file.close()
1248
1249     grub_flavour_config = True
1250     if os.path.isfile(grub1_cfg):
1251         string = open(grub1_cfg).readlines()
1252         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1253         for line in string:
1254             if flavour.match(line):
1255                 grub_flavour_config = False
1256
1257     if grub_flavour_config:
1258         grub1_config_file = open(grub1_cfg, 'a')
1259         grub1_config_file.write(generate_flavour_specific_grub1_config(grml_flavour, install_partition, bootopt))
1260         grub1_config_file.close()
1261
1262     # make sure grub.conf isn't a symlink but a plain file instead,
1263     # otherwise it will break on FAT16 filesystems
1264     # this works around grub-install of (at least) Fedora 10
1265     if os.path.isfile(grub1_cfg):
1266         grubconf = grub_target + 'grub.conf'
1267         if not os.path.islink(grubconf):
1268             import shutil
1269             shutil.copyfile(grub1_cfg, grub_target + 'grub.conf')
1270
1271 def handle_grub2_config(grml_flavour, grub_target, bootopt):
1272     """Main handler for generating grub2 configuration
1273
1274     @grml_flavour: name of grml flavour the configuration should be generated for
1275     @grub_target: path of grub's configuration files
1276     @bootoptions: additional bootoptions that should be used by default"""
1277
1278     # grub2 config
1279     grub2_cfg = grub_target + 'grub.cfg'
1280     logging.debug("Creating grub2 configuration file (grub.lst)")
1281
1282     global GRML_DEFAULT
1283
1284     # install main configuration only *once*, no matter how many ISOs we have:
1285     grub_flavour_is_default = False
1286     if os.path.isfile(grub2_cfg):
1287         string = open(grub2_cfg).readline()
1288         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1289         if not re.match(main_identifier, string):
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     else:
1296         grub2_config_file = open(grub2_cfg, 'w')
1297         GRML_DEFAULT = grml_flavour
1298         grub_flavour_is_default = True
1299         grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1300         grub2_config_file.close()
1301
1302     # install flavour specific configuration only *once* as well
1303     grub_flavour_config = True
1304     if os.path.isfile(grub2_cfg):
1305         string = open(grub2_cfg).readlines()
1306         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1307         for line in string:
1308             if flavour.match(line):
1309                 grub_flavour_config = False
1310
1311     if grub_flavour_config:
1312         grub2_config_file = open(grub2_cfg, 'a')
1313         # display only if the grml flavour isn't the default
1314         if not grub_flavour_is_default:
1315             GRML_FLAVOURS.add(grml_flavour)
1316         grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, bootopt))
1317         grub2_config_file.close()
1318
1319
1320 def handle_grub_config(grml_flavour, device, target):
1321     """Main handler for generating grub (v1 and v2) configuration
1322
1323     @grml_flavour: name of grml flavour the configuration should be generated for
1324     @device: device/partition where grub should be installed to
1325     @target: path of grub's configuration files"""
1326
1327     logging.debug("Generating grub configuration")
1328
1329     grub_target = target + '/boot/grub/'
1330     execute(mkdir, grub_target)
1331
1332     if os.path.isdir(device):
1333         install_grub1_partition = None
1334     else:
1335         if device[-1:].isdigit():
1336             install_grub1_partition = int(device[-1:]) - 1
1337         else:
1338             raise CriticalException("error validating partition schema (raw device?)")
1339
1340     # do NOT write "None" in kernel cmdline
1341     if options.bootoptions is None:
1342         bootopt = ""
1343     else:
1344         bootopt = options.bootoptions
1345
1346     # write menu.lst
1347     handle_grub1_config(grml_flavour, install_grub1_partition, grub_target, bootopt)
1348     # write grub.cfg
1349     handle_grub2_config(grml_flavour, grub_target, bootopt)
1350
1351
1352 def initial_syslinux_config(target):
1353     """Generates intial syslinux configuration
1354
1355     @target path of syslinux's configuration files"""
1356
1357     target = target + "/"
1358     filename = target + "grmlmain.cfg"
1359     if os.path.isfile(target + "grmlmain.cfg"):
1360         return
1361     data = open(filename, "w")
1362     data.write(generate_main_syslinux_config())
1363     data.close
1364
1365
1366 def adjust_syslinux_bootoptions(src_name, dst_name, flavour):
1367     append_re = re.compile("^(\s*append.*)$", re.I)
1368     boot_re = re.compile("/boot/([a-zA-Z0-9_]+/)+([a-zA-Z0-9._]+)")
1369     flavour_re = re.compile("(label.*)(grml\w+)")
1370     default_re = re.compile("(default.cfg)")
1371     src = open(src_name, "r")
1372     dst = open(dst_name, "w")
1373
1374     for line in src:
1375         line = boot_re.sub(r'/boot/release/%s/\2 ' % flavour, line)
1376         line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1377         line = default_re.sub(r'%s-\1' % flavour, line)
1378         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1379         dst.write(line)
1380     src.close()
1381     dst.close()
1382
1383
1384 def add_syslinux_entry(filename, grml_flavour):
1385     data = open(filename, "a+")
1386     entry_filename = "option-%s.cfg" % grml_flavour
1387     entry = "include %s\n" % entry_filename
1388     path = os.path.dirname(filename)
1389     for line in data:
1390         if line == entry:
1391             break
1392     else:
1393         data.write(entry)
1394
1395     data.close()
1396     data = open(path + "/" + entry_filename, "w")
1397     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1398     data.close()
1399
1400
1401 def handle_syslinux_config(grml_flavour, target):
1402     """Main handler for generating syslinux configuration
1403
1404     @grml_flavour: name of grml flavour the configuration should be generated for
1405     @target: path of syslinux's configuration files"""
1406
1407     # do NOT write "None" in kernel cmdline
1408     if options.bootoptions is None:
1409         bootopt = ""
1410     else:
1411         bootopt = options.bootoptions
1412
1413     logging.debug("Generating syslinux configuration")
1414     syslinux_target = target + '/boot/syslinux/'
1415     # should be present via  copy_bootloader_files(), but make sure it exits:
1416     execute(mkdir, syslinux_target)
1417     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1418
1419     global GRML_DEFAULT
1420
1421     # install main configuration only *once*, no matter how many ISOs we have:
1422     syslinux_flavour_is_default = False
1423     syslinux_config_file = open(syslinux_cfg, 'w')
1424     syslinux_config_file.write("include vesamenu.cfg")
1425     syslinux_config_file.close()
1426
1427     initial_syslinux_config(syslinux_target)
1428     for filename in 'grml.cfg', 'default.cfg', 'hidden.cfg':
1429         old_filename = "%s/%s" % (syslinux_target, filename)
1430         new_filename = "%s/%s-%s" % (syslinux_target, grml_flavour, filename)
1431         adjust_syslinux_bootoptions(old_filename, new_filename, grml_flavour)
1432         os.unlink(old_filename)
1433
1434     new_hidden =  "%s-hidden.cfg" % (grml_flavour)
1435     new_default = "%s-default.cfg" % (grml_flavour)
1436     default_file = open("%s/defaults.cfg" % syslinux_target, "a+")
1437     entry = "include %s\n" % new_default
1438     for line in default_file:
1439         if line == entry:
1440             break
1441     else:
1442         default_file.write("include %s\n" % new_default)
1443
1444     default_file.close()
1445     add_syslinux_entry("%s/additional.cfg" % syslinux_target, grml_flavour)
1446
1447
1448 def handle_bootloader_config(grml_flavour, device, target):
1449     """Main handler for generating bootloader's configuration
1450
1451     @grml_flavour: name of grml flavour the configuration should be generated for
1452     @device: device/partition where bootloader should be installed to
1453     @target: path of bootloader's configuration files"""
1454
1455     if options.skipsyslinuxconfig:
1456         logging.info("Skipping generation of syslinux configuration as requested.")
1457     else:
1458         try:
1459             handle_syslinux_config(grml_flavour, target)
1460         except CriticalException, error:
1461             logging.critical("Fatal: %s", error)
1462             sys.exit(1)
1463
1464     if options.skipgrubconfig:
1465         logging.info("Skipping generation of grub configuration as requested.")
1466     else:
1467         try:
1468             handle_grub_config(grml_flavour, device, target)
1469         except CriticalException, error:
1470             logging.critical("Fatal: %s", error)
1471             sys.exit(1)
1472
1473
1474 def handle_dir(live_image, device):
1475     """Main logic for copying files of the currently running grml system.
1476
1477     @live_image: directory where currently running live system resides (usually /live/image)
1478     @device: partition where the specified ISO should be installed to"""
1479
1480     logging.info("Using %s as install base", live_image)
1481
1482     if os.path.isdir(device):
1483         logging.info("Specified target is a directory, therefore not mounting.")
1484         device_mountpoint = device
1485         remove_device_mountpoint = False
1486     else:
1487         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1488         register_tmpfile(device_mountpoint)
1489         remove_device_mountpoint = True
1490         try:
1491             mount(device, device_mountpoint, "")
1492         except CriticalException, error:
1493             logging.critical("Fatal: %s", error)
1494             cleanup()
1495             sys.exit(1)
1496
1497     try:
1498         try:
1499             grml_flavour = identify_grml_flavour(live_image)
1500             logging.info("Identified grml flavour \"%s\".", grml_flavour)
1501             install_iso_files(grml_flavour, live_image, device, device_mountpoint)
1502         except TypeError:
1503             logging.critical("Fatal: a critical error happend during execution (not a grml ISO?), giving up")
1504             sys.exit(1)
1505     finally:
1506         if remove_device_mountpoint:
1507             try:
1508                 unmount(device_mountpoint, "")
1509                 if os.path.isdir(device_mountpoint):
1510                     os.rmdir(device_mountpoint)
1511                     unregister_tmpfile(device_mountpoint)
1512             except CriticalException, error:
1513                 logging.critical("Fatal: %s", error)
1514                 cleanup()
1515
1516
1517 def handle_iso(iso, device):
1518     """Main logic for mounting ISOs and copying files.
1519
1520     @iso: full path to the ISO that should be installed to the specified device
1521     @device: partition where the specified ISO should be installed to"""
1522
1523     logging.info("Using ISO %s", iso)
1524
1525     iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1526     register_tmpfile(iso_mountpoint)
1527     remove_iso_mountpoint = True
1528
1529     if not os.path.isfile(iso):
1530         logging.critical("Fatal: specified ISO %s could not be read", iso)
1531         cleanup()
1532         sys.exit(1)
1533
1534     try:
1535         mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
1536     except CriticalException, error:
1537         logging.critical("Fatal: %s", error)
1538         sys.exit(1)
1539
1540     if os.path.isdir(device):
1541         logging.info("Specified target is a directory, therefore not mounting.")
1542         device_mountpoint = device
1543         remove_device_mountpoint = False
1544         # skip_mbr = True
1545     else:
1546         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1547         register_tmpfile(device_mountpoint)
1548         remove_device_mountpoint = True
1549         try:
1550             mount(device, device_mountpoint, "")
1551         except CriticalException, error:
1552             logging.critical("Fatal: %s", error)
1553             cleanup()
1554             sys.exit(1)
1555
1556     try:
1557         try:
1558             grml_flavour = identify_grml_flavour(iso_mountpoint)
1559             logging.info("Identified grml flavour \"%s\".", grml_flavour)
1560             install_iso_files(grml_flavour, iso_mountpoint, device, device_mountpoint)
1561         except TypeError:
1562             logging.critical("Fatal: a critical error happend during execution (not a grml ISO?), giving up")
1563             sys.exit(1)
1564     finally:
1565         if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
1566             unmount(iso_mountpoint, "")
1567             os.rmdir(iso_mountpoint)
1568             unregister_tmpfile(iso_mountpoint)
1569         if remove_device_mountpoint:
1570             try:
1571                 unmount(device_mountpoint, "")
1572                 if os.path.isdir(device_mountpoint):
1573                     os.rmdir(device_mountpoint)
1574                     unregister_tmpfile(device_mountpoint)
1575             except CriticalException, error:
1576                 logging.critical("Fatal: %s", error)
1577                 cleanup()
1578
1579
1580 def handle_mbr(device):
1581     """Main handler for installing master boot record (MBR)
1582
1583     @device: device where the MBR should be installed to"""
1584
1585     if options.dryrun:
1586         logging.info("Would install MBR")
1587         return 0
1588
1589     if device[-1:].isdigit():
1590         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1591         partition_number = int(device[-1:]) - 1
1592         skip_install_mir_mbr = False
1593
1594     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1595     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1596     if mbr_device == "/dev/loop":
1597         mbr_device = device
1598         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1599         skip_install_mir_mbr = True
1600
1601     try:
1602         if options.syslinuxmbr:
1603             handle_syslinux_mbr(mbr_device)
1604         elif not skip_install_mir_mbr:
1605             if options.mbrmenu:
1606                 install_mir_mbr(GRML2USB_BASE + '/mbr/mbrmgr', mbr_device, partition_number, True)
1607             else:
1608                 install_mir_mbr(GRML2USB_BASE + '/mbr/mbrldr', mbr_device, partition_number, False)
1609     except IOError, error:
1610         logging.critical("Execution failed: %s", error)
1611         sys.exit(1)
1612     except Exception, error:
1613         logging.critical("Execution failed: %s", error)
1614         sys.exit(1)
1615
1616
1617 def handle_vfat(device):
1618     """Check for FAT specific settings and options
1619
1620     @device: device that should checked / formated"""
1621
1622     # make sure we have mkfs.vfat available
1623     if options.fat16:
1624         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1625             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1626             logging.critical('Please make sure to install dosfstools.')
1627             sys.exit(1)
1628
1629         exec_mkfs = False
1630         if options.force:
1631             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1632             exec_mkfs = True
1633         else:
1634             # make sure the user is aware of what he is doing
1635             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1636             if f == "y" or f == "Y":
1637                 logging.info("Note: you can skip this question using the option --force")
1638                 exec_mkfs = True
1639
1640         if exec_mkfs:
1641             try:
1642                 mkfs_fat16(device)
1643             except CriticalException, error:
1644                 logging.critical("Execution failed: %s", error)
1645                 sys.exit(1)
1646         else:
1647             sys.exit(1)
1648
1649     # check for vfat filesystem
1650     if device is not None and not os.path.isdir(device):
1651         try:
1652             check_for_fat(device)
1653         except CriticalException, error:
1654             logging.critical("Execution failed: %s", error)
1655             sys.exit(1)
1656
1657     if not os.path.isdir(device) and not check_for_usbdevice(device):
1658         print "Warning: the specified device %s does not look like a removable usb device." % device
1659         f = raw_input("Do you really want to continue? y/N ")
1660         if f == "y" or f == "Y":
1661             pass
1662         else:
1663             sys.exit(1)
1664
1665
1666 def handle_compat_warning(device):
1667     """Backwards compatible checks
1668
1669     @device: device that should be checked"""
1670
1671     # make sure we can replace old grml2usb script and warn user when using old way of life:
1672     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1673         print "Warning: the semantics of grml2usb has changed."
1674         print "Instead of using grml2usb /path/to/iso %s you might" % device
1675         print "want to use grml2usb /path/to/iso /dev/... instead."
1676         print "Please check out the grml2usb manpage for details."
1677         f = raw_input("Do you really want to continue? y/N ")
1678         if f == "y" or f == "Y":
1679             pass
1680         else:
1681             sys.exit(1)
1682
1683
1684 def handle_logging():
1685     """Log handling and configuration"""
1686
1687     if options.verbose and options.quiet:
1688         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1689
1690     if options.verbose:
1691         FORMAT = "Debug: %(asctime)-15s %(message)s"
1692         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1693     elif options.quiet:
1694         FORMAT = "Critical: %(message)s"
1695         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1696     else:
1697         FORMAT = "%(message)s"
1698         logging.basicConfig(level=logging.INFO, format=FORMAT)
1699
1700
1701 def handle_bootloader(device):
1702     """wrapper for installing bootloader
1703
1704     @device: device where bootloader should be installed to"""
1705
1706     # Install bootloader only if not using the --copy-only option
1707     if options.copyonly:
1708         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1709     elif os.path.isdir(device):
1710         logging.info("Not installing bootloader as %s is a directory.", device)
1711     else:
1712         install_bootloader(device)
1713
1714
1715 def main():
1716     """Main function [make pylint happy :)]"""
1717
1718     if options.version:
1719         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1720         sys.exit(0)
1721
1722     if len(args) < 2:
1723         parser.error("invalid usage")
1724
1725     # log handling
1726     handle_logging()
1727
1728     # make sure we have the appropriate permissions
1729     check_uid_root()
1730
1731     logging.info("Executing grml2usb version %s", PROG_VERSION)
1732
1733     if options.dryrun:
1734         logging.info("Running in simulation mode as requested via option dry-run.")
1735
1736     # specified arguments
1737     device = args[len(args) - 1]
1738     isos = args[0:len(args) - 1]
1739
1740     if not os.path.isdir(device):
1741         if device[-1:].isdigit():
1742             if int(device[-1:]) > 4 or device[-2:].isdigit():
1743                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1744                 sys.exit(1)
1745         else:
1746             if os.path.exists(device):
1747                 logging.critical("Fatal: installation on raw device not supported. (BIOS won't support it.)")
1748                 sys.exit(1)
1749
1750     if not which("rsync"):
1751         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1752         sys.exit(1)
1753
1754     # provide upgrade path
1755     handle_compat_warning(device)
1756
1757     # check for vfat partition
1758     handle_vfat(device)
1759
1760     # main operation (like installing files)
1761     for iso in isos:
1762         if os.path.isdir(iso):
1763             handle_dir(iso, device)
1764         else:
1765             handle_iso(iso, device)
1766
1767     # install mbr
1768     if not os.path.isdir(device):
1769         if not options.skipmbr:
1770             handle_mbr(device)
1771
1772     handle_bootloader(device)
1773
1774     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1775
1776     for flavour in GRML_FLAVOURS:
1777         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1778
1779     # finally be politely :)
1780     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1781
1782 if __name__ == "__main__":
1783     try:
1784         main()
1785     except KeyboardInterrupt:
1786         logging.info("Received KeyboardInterrupt")
1787         cleanup()
1788
1789 ## END OF FILE #################################################################
1790 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8