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