39e441f7eb9c80a35908354ecdf4ebcfb850ca32
[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     kernel = search_file('vmlinuz', iso_mount)
947     if kernel is None:
948         # compat for releases < 2011.12
949         kernel = search_file('linux26', iso_mount)
950
951     if kernel is None:
952         logging.critical("Fatal: kernel not found")
953         raise CriticalException("error locating kernel file")
954
955     source = os.path.dirname(kernel) + '/'
956     dest = target + '/' + os.path.dirname(kernel).replace(iso_mount,'') + '/'
957     execute(mkdir, dest)
958     exec_rsync(source, dest)
959
960
961 def update_grml_versions(iso_mount, target):
962     """Update the grml version file on a cd
963     Returns true if version was updated successfully,
964     False if grml-version does not exist yet on the mountpoint
965
966     @iso_mount: string of the iso mount point
967     @target: path of the target mount point
968     """
969     grml_target = target + '/grml/'
970     target_grml_version_file = search_file('grml-version', grml_target)
971     if target_grml_version_file:
972         iso_grml_version_file = search_file('grml-version', iso_mount)
973         if not iso_grml_version_file:
974             logging.warn("Warning: %s could not be found - can not install it", iso_grml_version_file)
975             return False
976         try:
977             # read the flavours from the iso image
978             iso_versions = {}
979             iso_file = open(iso_grml_version_file, 'r')
980             for line in iso_file:
981                 iso_versions[get_flavour(line)] = line.strip()
982
983             # update the existing flavours on the target
984             for line in fileinput.input([target_grml_version_file], inplace=1):
985                 flavour = get_flavour(line)
986                 if flavour in iso_versions.keys():
987                     print iso_versions.pop(flavour)
988                 else:
989                     print line.strip()
990             fileinput.close()
991
992             target_file = open(target_grml_version_file, 'a')
993             # add the new flavours from the current iso
994             for flavour in iso_versions:
995                 target_file.write("%s\n" % iso_versions[flavour])
996         except IOError:
997             logging.warn("Warning: Could not write file")
998         finally:
999             iso_file.close()
1000             target_file.close()
1001         return True
1002     else:
1003         return False
1004
1005 def copy_grml_files(iso_mount, target):
1006     """copy some minor grml files to a given target
1007
1008     @iso_mount: path where a grml ISO is mounted on
1009     @target: path where grml's main files should be copied to"""
1010
1011     grml_target = target + '/grml/'
1012     execute(mkdir, grml_target)
1013
1014     copy_files = [ 'grml-cheatcodes.txt', 'LICENSE.txt', 'md5sums', 'README.txt' ]
1015     # handle grml-version
1016     if not update_grml_versions(iso_mount, target):
1017         copy_files.append('grml-version')
1018
1019     for myfile in copy_files:
1020         grml_file = search_file(myfile, iso_mount)
1021         if grml_file is None:
1022             logging.warn("Warning: file %s could not be found - can not install it", myfile)
1023         else:
1024             exec_rsync(grml_file, grml_target + myfile)
1025
1026     grml_web_target = grml_target + '/web/'
1027     execute(mkdir, grml_web_target)
1028
1029     for myfile in 'index.html', 'style.css':
1030         grml_file = search_file(myfile, iso_mount)
1031         if grml_file is None:
1032             logging.warn("Warning: file %s could not be found - can not install it", myfile)
1033         else:
1034             exec_rsync(grml_file, grml_web_target + myfile)
1035
1036     grml_webimg_target = grml_web_target + '/images/'
1037     execute(mkdir, grml_webimg_target)
1038
1039     for myfile in 'button.png', 'favicon.png', 'linux.jpg', 'logo.png':
1040         grml_file = search_file(myfile, iso_mount)
1041         if grml_file is None:
1042             logging.warn("Warning: file %s could not be found - can not install it", myfile)
1043         else:
1044             exec_rsync(grml_file, grml_webimg_target + myfile)
1045
1046
1047 def handle_addon_copy(filename, dst, iso_mount, ignore_errors=False):
1048     """handle copy of optional addons
1049
1050     @filename: filename of the addon
1051     @dst: destination directory
1052     @iso_mount: location of the iso mount
1053     @ignore_errors: don't report missing files
1054     """
1055     file_location = search_file(filename, iso_mount)
1056     if file_location is None:
1057         if not ignore_errors:
1058             logging.warn("Warning: %s not found (that's fine if you don't need it)",  filename)
1059     else:
1060         exec_rsync(file_location, dst)
1061
1062
1063 def copy_addons(iso_mount, target):
1064     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1065
1066     @iso_mount: path where a grml ISO is mounted on
1067     @target: path where grml's main files should be copied to"""
1068
1069     addons = target + '/boot/addons/'
1070     execute(mkdir, addons)
1071
1072     # grub all-in-one image
1073     handle_addon_copy('allinone.img', addons, iso_mount)
1074
1075     # bsd imag
1076     handle_addon_copy('bsd4grml', addons, iso_mount)
1077
1078     handle_addon_copy('balder10.imz', addons, iso_mount)
1079
1080     # install hdt and pci.ids only when using syslinux (grub doesn't support it)
1081     if options.syslinux:
1082         # hdt (hardware detection tool) image
1083         hdtimg = search_file('hdt.c32', iso_mount)
1084         if hdtimg:
1085             exec_rsync(hdtimg, addons + '/hdt.c32')
1086
1087         # pci.ids file
1088         picids = search_file('pci.ids', iso_mount)
1089         if picids:
1090             exec_rsync(picids, addons + '/pci.ids')
1091
1092     # memdisk image
1093     handle_addon_copy('memdisk', addons, iso_mount)
1094
1095     # memtest86+ image
1096     handle_addon_copy('memtest', addons, iso_mount)
1097
1098     # gpxe.lkrn: got replaced by ipxe
1099     handle_addon_copy('gpxe.lkrn', addons, iso_mount, ignore_errors=True)
1100
1101     # ipxe.lkrn
1102     handle_addon_copy('ipxe.lkrn', addons, iso_mount)
1103
1104 def glob_and_copy(filepattern, dst):
1105     """Glob on specified filepattern and copy the result to dst
1106
1107     @filepattern: globbing pattern
1108     @dst: target directory
1109     """
1110     for name in glob.glob(filepattern):
1111         copy_if_exist(name, dst)
1112
1113 def search_and_copy(filename, search_path, dst):
1114     """Search for the specified filename at searchpath and copy it to dst
1115
1116     @filename: filename to look for
1117     @search_path: base search file
1118     @dst: destionation to copy the file to
1119     """
1120     file_location = search_file(filename, search_path)
1121     copy_if_exist(file_location, dst)
1122
1123 def copy_if_exist(filename, dst):
1124     """Copy filename to dst if filename is set.
1125
1126     @filename: a filename
1127     @dst: dst file
1128     """
1129     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1130         exec_rsync(filename, dst)
1131
1132 def copy_bootloader_files(iso_mount, target, grml_flavour):
1133     """Copy grml's bootloader files to a given target
1134
1135     @iso_mount: path where a grml ISO is mounted on
1136     @target: path where grml's main files should be copied to
1137     @grml_flavour: name of the current processed grml_flavour
1138     """
1139
1140     syslinux_target = target + '/boot/syslinux/'
1141     execute(mkdir, syslinux_target)
1142
1143     grub_target = target + '/boot/grub/'
1144     execute(mkdir, grub_target)
1145
1146     logo = search_file('logo.16', iso_mount)
1147     exec_rsync(logo, syslinux_target + 'logo.16')
1148
1149     bootx64_efi = search_file('bootx64.efi', iso_mount)
1150     if bootx64_efi:
1151         mkdir(target + '/efi/boot/')
1152         exec_rsync(bootx64_efi, target + '/efi/boot/bootx64.efi')
1153
1154     efi_img = search_file('efi.img', iso_mount)
1155     if efi_img:
1156         mkdir(target + '/boot/')
1157         exec_rsync(efi_img, target + '/boot/efi.img')
1158
1159     for ffile in ['f%d' % number for number in range(1, 11) ]:
1160         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1161
1162     loopback_cfg = search_file("loopback.cfg", iso_mount)
1163     if loopback_cfg:
1164         directory = os.path.dirname(loopback_cfg)
1165         directory = directory.replace(iso_mount, "")
1166         mkdir(os.path.join(target, directory))
1167         exec_rsync(loopback_cfg, target + os.path.sep + directory)
1168
1169     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1170     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1171     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1172         os.unlink(syslinux_target + 'ldlinux.sys')
1173
1174     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1175     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1176
1177     if not source_dir:
1178         logging.critical("Fatal: file default.cfg could not be found.")
1179         logging.critical("Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...")
1180         logging.critical("       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1181         raise
1182
1183     for expr in name, 'distri.cfg', \
1184         defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1185         'isoprompt.cfg', 'options.cfg', \
1186         'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1187         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1188
1189     for filename in glob.glob1(syslinux_target, "*.c32"):
1190         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1191
1192
1193     # copy the addons_*.cfg file to the new syslinux directory
1194     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1195
1196     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1197
1198     # copy all grub files from ISO
1199     glob_and_copy(iso_mount + '/boot/grub/*.mod', grub_target)
1200     glob_and_copy(iso_mount + '/boot/grub/*.lst', grub_target)
1201     glob_and_copy(iso_mount + '/boot/grub/*.img', grub_target)
1202     glob_and_copy(iso_mount + '/boot/grub/*.pf2', grub_target) # fonts for splash
1203     glob_and_copy(iso_mount + '/boot/grub/*.png', grub_target) # splash image
1204     glob_and_copy(iso_mount + '/boot/grub/stage*', grub_target)
1205
1206 def install_iso_files(grml_flavour, iso_mount, device, target):
1207     """Copy files from ISO to given target
1208
1209     @grml_flavour: name of grml flavour the configuration should be generated for
1210     @iso_mount: path where a grml ISO is mounted on
1211     @device: device/partition where bootloader should be installed to
1212     @target: path where grml's main files should be copied to"""
1213
1214     global GRML_DEFAULT
1215     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1216     if options.dryrun:
1217         return 0
1218     elif not options.bootloaderonly:
1219         logging.info("Copying files. This might take a while....")
1220         try:
1221             copy_system_files(grml_flavour, iso_mount, target)
1222             copy_grml_files(iso_mount, target)
1223         except CriticalException, error:
1224             logging.critical("Execution failed: %s", error)
1225             sys.exit(1)
1226
1227     if not options.skipaddons:
1228         if not search_file('addons', iso_mount):
1229             logging.info("Could not find addons, therefore not installing.")
1230         else:
1231             copy_addons(iso_mount, target)
1232
1233     if not options.copyonly:
1234         copy_bootloader_files(iso_mount, target, grml_flavour)
1235
1236         if not options.dryrun:
1237             handle_bootloader_config(grml_flavour, device, target)
1238
1239     # make sure we sync filesystems before returning
1240     proc = subprocess.Popen(["sync"])
1241     proc.wait()
1242
1243
1244 def get_flavour(flavour_str):
1245     """Returns the flavour of a grml version string
1246     """
1247     return re.match(r'[\w-]*', flavour_str).group()
1248
1249 def identify_grml_flavour(mountpath):
1250     """Get name of grml flavour
1251
1252     @mountpath: path where the grml ISO is mounted to
1253     @return: name of grml-flavour"""
1254
1255     version_file = search_file('grml-version', mountpath)
1256
1257     if version_file == "":
1258         logging.critical("Error: could not find grml-version file.")
1259         raise
1260
1261     flavours = []
1262     tmpfile = None
1263     try:
1264         tmpfile = open(version_file, 'r')
1265         for line in tmpfile.readlines():
1266             flavours.append(get_flavour(line))
1267     except TypeError, e:
1268         raise
1269     except Exception, e:
1270         logging.critical("Unexpected error: %s", e)
1271         raise
1272     finally:
1273         if tmpfile:
1274             tmpfile.close()
1275
1276     return flavours
1277
1278
1279 def modify_grub_config(filename):
1280     """Adjust bootoptions for a grub file
1281
1282     @filename: filename to modify
1283     """
1284     if options.removeoption:
1285         regexe = []
1286         for regex in options.removeoption:
1287             regexe.append(re.compile(r'%s' % regex))
1288
1289         option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1290
1291         for line in fileinput.input(filename, inplace=1):
1292             if regexe and option_re.search(line):
1293                 for regex in regexe:
1294                     line = regex.sub(' ', line)
1295
1296             sys.stdout.write(line)
1297
1298         fileinput.close()
1299
1300 def handle_grub2_config(grml_flavour, grub_target, bootopt):
1301     """Main handler for generating grub2 configuration
1302
1303     @grml_flavour: name of grml flavour the configuration should be generated for
1304     @grub_target: path of grub's configuration files
1305     @bootoptions: additional bootoptions that should be used by default"""
1306
1307     # grub2 config
1308     grub2_cfg = grub_target + 'grub.cfg'
1309     logging.debug("Creating grub2 configuration file (grub.cfg)")
1310
1311     global GRML_DEFAULT
1312
1313     # install main configuration only *once*, no matter how many ISOs we have:
1314     install_main_config = True
1315     if os.path.isfile(grub2_cfg):
1316         string = open(grub2_cfg).readline()
1317         main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1318         if re.match(main_identifier, string):
1319             install_main_config = False
1320     if install_main_config:
1321         grub2_config_file = open(grub2_cfg, 'w')
1322         GRML_DEFAULT = grml_flavour
1323         grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1324         grub2_config_file.close()
1325
1326     # install flavour specific configuration only *once* as well
1327     grub_flavour_config = True
1328     if os.path.isfile(grub2_cfg):
1329         string = open(grub2_cfg).readlines()
1330         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1331         for line in string:
1332             if flavour.match(line):
1333                 grub_flavour_config = False
1334
1335     if grub_flavour_config:
1336         grub2_config_file = open(grub2_cfg, 'a')
1337         # display only if the grml flavour isn't the default
1338         if GRML_DEFAULT != grml_flavour:
1339             GRML_FLAVOURS.add(grml_flavour)
1340         grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, bootopt))
1341         grub2_config_file.close()
1342
1343     modify_grub_config(grub2_cfg)
1344
1345
1346 def get_bootoptions(grml_flavour):
1347     """Returns bootoptions for specific flavour
1348
1349     @grml_flavour: name of the grml_flavour
1350     """
1351     # do NOT write "None" in kernel cmdline
1352     if not options.bootoptions:
1353         bootopt = ""
1354     else:
1355         bootopt = " ".join(options.bootoptions)
1356     bootopt = bootopt.replace("%flavour", grml_flavour)
1357     return bootopt
1358
1359
1360 def handle_grub_config(grml_flavour, device, target):
1361     """Main handler for generating grub (v1 and v2) configuration
1362
1363     @grml_flavour: name of grml flavour the configuration should be generated for
1364     @device: device/partition where grub should be installed to
1365     @target: path of grub's configuration files"""
1366
1367     logging.debug("Generating grub configuration")
1368
1369     grub_target = target + '/boot/grub/'
1370
1371     bootopt = get_bootoptions(grml_flavour)
1372
1373     # write grub.cfg
1374     handle_grub2_config(grml_flavour, grub_target, bootopt)
1375
1376
1377 def initial_syslinux_config(target):
1378     """Generates intial syslinux configuration
1379
1380     @target path of syslinux's configuration files"""
1381
1382     target = target + "/"
1383     filename = target + "grmlmain.cfg"
1384     if os.path.isfile(target + "grmlmain.cfg"):
1385         return
1386     data = open(filename, "w")
1387     data.write(generate_main_syslinux_config())
1388     data.close()
1389
1390     filename = target + "hiddens.cfg"
1391     data = open(filename, "w")
1392     data.write("include hidden.cfg\n")
1393     data.close()
1394
1395 def add_entry_if_not_present(filename, entry):
1396     """Write entry into filename if entry is not already in the file
1397
1398     @filanme: name of the file
1399     @entry: data to write to the file
1400     """
1401     data = open(filename, "a+")
1402     for line in data:
1403         if line == entry:
1404             break
1405     else:
1406         data.write(entry)
1407
1408     data.close()
1409
1410 def get_flavour_filename(flavour):
1411     """Generate a iso9960 save filename out of the specified flavour
1412
1413     @flavour: grml flavour
1414     """
1415     return flavour.replace('-', '_')
1416
1417 def adjust_syslinux_bootoptions(src, flavour):
1418     """Adjust existing bootoptions of specified syslinux config to
1419     grml2usb specific ones, e.g. change the location of the kernel...
1420
1421     @src: config file to alter
1422     @flavour: grml flavour
1423     """
1424
1425     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1426     # flavour_re = re.compile("(label.*)(grml\w+)")
1427     default_re = re.compile("(default.cfg)")
1428     bootid_re = re.compile("bootid=[\w_-]+")
1429     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1430
1431     bootopt = get_bootoptions(flavour)
1432
1433     regexe = []
1434     option_re = None
1435     if options.removeoption:
1436         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1437
1438         for regex in options.removeoption:
1439             regexe.append(re.compile(r'%s' % regex))
1440
1441     for line in fileinput.input(src, inplace=1):
1442         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1443         line = default_re.sub(r'%s-\1' % flavour, line)
1444         line = bootid_re.sub('', line)
1445         line = live_media_path_re.sub('', line)
1446         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1447         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1448         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1449         if option_re and option_re.search(line):
1450             for regex in regexe:
1451                 line = regex.sub(' ', line)
1452         sys.stdout.write(line)
1453     fileinput.close()
1454
1455 def adjust_labels(src, replacement):
1456     """Adjust the specified labels in the syslinux config file src with
1457     specified replacement
1458     """
1459     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1460     for line in fileinput.input(src, inplace=1):
1461         line = label_re.sub(replacement, line)
1462         sys.stdout.write(line)
1463     fileinput.close()
1464
1465
1466 def add_syslinux_entry(filename, grml_flavour):
1467     """Add includes for a specific grml_flavour to the specified filename
1468
1469     @filename: syslinux config file
1470     @grml_flavour: grml flavour to add
1471     """
1472
1473     entry_filename = "option_%s.cfg" % grml_flavour
1474     entry = "include %s\n" % entry_filename
1475
1476     add_entry_if_not_present(filename, entry)
1477     path = os.path.dirname(filename)
1478
1479     data = open(path + "/" + entry_filename, "w")
1480     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1481     data.close()
1482
1483 def modify_filenames(grml_flavour, target, filenames):
1484     """Replace the standard filenames with the new ones
1485
1486     @grml_flavour: grml-flavour strin
1487     @target: directory where the files are located
1488     @filenames: list of filenames to alter
1489     """
1490     grml_filename = grml_flavour.replace('-', '_')
1491     for filename in filenames:
1492         old_filename = "%s/%s" % (target, filename)
1493         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1494         os.rename(old_filename, new_filename)
1495
1496
1497 def remove_default_entry(filename):
1498     """Remove the default entry from specified syslinux file
1499
1500     @filename: syslinux config file
1501     """
1502     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1503     for line in fileinput.input(filename, inplace=1):
1504         if default_re.match(line):
1505             continue
1506         sys.stdout.write(line)
1507     fileinput.close()
1508
1509
1510 def handle_syslinux_config(grml_flavour, target):
1511     """Main handler for generating syslinux configuration
1512
1513     @grml_flavour: name of grml flavour the configuration should be generated for
1514     @target: path of syslinux's configuration files"""
1515
1516     logging.debug("Generating syslinux configuration")
1517     syslinux_target = target + '/boot/syslinux/'
1518     # should be present via  copy_bootloader_files(), but make sure it exits:
1519     execute(mkdir, syslinux_target)
1520     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1521
1522
1523     # install main configuration only *once*, no matter how many ISOs we have:
1524     syslinux_config_file = open(syslinux_cfg, 'w')
1525     syslinux_config_file.write("TIMEOUT 300\n")
1526     syslinux_config_file.write("include vesamenu.cfg\n")
1527     syslinux_config_file.close()
1528
1529     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1530     prompt_name.write('menu label S^yslinux prompt\n')
1531     prompt_name.close()
1532
1533     initial_syslinux_config(syslinux_target)
1534     flavour_filename = grml_flavour.replace('-', '_')
1535
1536     if search_file('default.cfg', syslinux_target):
1537         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1538
1539     filename = search_file("new_hidden.cfg", syslinux_target)
1540
1541
1542     # process hidden file
1543     if not search_file("hidden.cfg", syslinux_target):
1544         new_hidden = syslinux_target + "hidden.cfg"
1545         os.rename(filename, new_hidden)
1546         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1547     else:
1548         new_hidden_file =  "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1549         os.rename(filename, new_hidden_file)
1550         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1551         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1552         entry = 'include %s_hidden.cfg\n' % flavour_filename
1553         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1554
1555
1556
1557     new_default = "%s_default.cfg" % (flavour_filename)
1558     entry = 'include %s\n' % new_default
1559     defaults_file = '%s/defaults.cfg' % syslinux_target
1560     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1561     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1562
1563     if os.path.isfile(defaults_file):
1564
1565         # remove default menu entry in menu
1566         remove_default_entry(new_default_with_path)
1567
1568         # adjust all labels for additional isos
1569         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1570         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1571
1572     # always adjust bootoptions
1573     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1574     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1575
1576     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1577
1578     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1579
1580
1581 def handle_bootloader_config(grml_flavour, device, target):
1582     """Main handler for generating bootloader's configuration
1583
1584     @grml_flavour: name of grml flavour the configuration should be generated for
1585     @device: device/partition where bootloader should be installed to
1586     @target: path of bootloader's configuration files"""
1587
1588     global UUID
1589     UUID = get_uuid(target)
1590     if options.skipsyslinuxconfig:
1591         logging.info("Skipping generation of syslinux configuration as requested.")
1592     else:
1593         try:
1594             handle_syslinux_config(grml_flavour, target)
1595         except CriticalException, error:
1596             logging.critical("Fatal: %s", error)
1597             sys.exit(1)
1598
1599     if options.skipgrubconfig:
1600         logging.info("Skipping generation of grub configuration as requested.")
1601     else:
1602         try:
1603             handle_grub_config(grml_flavour, device, target)
1604         except CriticalException, error:
1605             logging.critical("Fatal: %s", error)
1606             sys.exit(1)
1607
1608
1609
1610 def install(image, device):
1611     """Install a grml image to the specified device
1612
1613     @image: directory or is file
1614     @device: partition or directory to install the device
1615     """
1616     iso_mountpoint = image
1617     remove_image_mountpoint = False
1618     if os.path.isdir(image):
1619         logging.info("Using %s as install base", image)
1620     else:
1621         logging.info("Using ISO %s", image)
1622         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1623         register_tmpfile(iso_mountpoint)
1624         remove_image_mountpoint = True
1625         try:
1626             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1627         except CriticalException, error:
1628             logging.critical("Fatal: %s", error)
1629             sys.exit(1)
1630
1631     try:
1632         install_grml(iso_mountpoint, device)
1633     finally:
1634         if remove_image_mountpoint:
1635             try:
1636                 remove_mountpoint(iso_mountpoint)
1637             except CriticalException, error:
1638                 logging.critical("Fatal: %s", error)
1639                 cleanup()
1640
1641
1642
1643 def install_grml(mountpoint, device):
1644     """Main logic for copying files of the currently running grml system.
1645
1646     @mountpoin: directory where currently running live system resides (usually /live/image)
1647     @device: partition where the specified ISO should be installed to"""
1648
1649     device_mountpoint = device
1650     if os.path.isdir(device):
1651         logging.info("Specified device is not a directory, therefore not mounting.")
1652         remove_device_mountpoint = False
1653     else:
1654         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1655         register_tmpfile(device_mountpoint)
1656         remove_device_mountpoint = True
1657         try:
1658             check_for_fat(device)
1659             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1660         except CriticalException, error:
1661             try:
1662                 mount(device, device_mountpoint, "")
1663             except CriticalException, error:
1664                 logging.critical("Fatal: %s", error)
1665                 raise
1666     try:
1667         grml_flavours = identify_grml_flavour(mountpoint)
1668         for flavour in set(grml_flavours):
1669             if not flavour:
1670                 logging.warning("No valid flavour found, please check your iso")
1671             logging.info("Identified grml flavour \"%s\".", flavour)
1672             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1673             GRML_FLAVOURS.add(flavour)
1674     finally:
1675         if remove_device_mountpoint:
1676             remove_mountpoint(device_mountpoint)
1677
1678 def remove_mountpoint(mountpoint):
1679     """remove a registred mountpoint
1680     """
1681
1682     try:
1683         unmount(mountpoint, "")
1684         if os.path.isdir(mountpoint):
1685             os.rmdir(mountpoint)
1686             unregister_tmpfile(mountpoint)
1687     except CriticalException, error:
1688         logging.critical("Fatal: %s", error)
1689         cleanup()
1690
1691 def handle_mbr(device):
1692     """Main handler for installing master boot record (MBR)
1693
1694     @device: device where the MBR should be installed to"""
1695
1696     if options.dryrun:
1697         logging.info("Would install MBR")
1698         return 0
1699
1700     if device[-1:].isdigit():
1701         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1702         partition_number = int(device[-1:]) - 1
1703     else:
1704         logging.warn("Could not detect partition number, not activating partition")
1705         partition_number = None
1706
1707     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1708     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1709     if mbr_device == "/dev/loop":
1710         mbr_device = device
1711         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1712
1713     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1714     if options.syslinuxmbr:
1715         mbrcode = '/usr/lib/syslinux/mbr.bin'
1716     elif options.mbrmenu:
1717         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1718
1719     try:
1720         install_mbr(mbrcode, mbr_device, partition_number, True)
1721     except IOError, error:
1722         logging.critical("Execution failed: %s", error)
1723         sys.exit(1)
1724     except Exception, error:
1725         logging.critical("Execution failed: %s", error)
1726         sys.exit(1)
1727
1728
1729 def handle_vfat(device):
1730     """Check for FAT specific settings and options
1731
1732     @device: device that should checked / formated"""
1733
1734     # make sure we have mkfs.vfat available
1735     if options.fat16:
1736         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1737             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1738             logging.critical('Please make sure to install dosfstools.')
1739             sys.exit(1)
1740
1741         if options.force:
1742             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1743         else:
1744             # make sure the user is aware of what he is doing
1745             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1746             if f == "y" or f == "Y":
1747                 logging.info("Note: you can skip this question using the option --force")
1748             else:
1749                 sys.exit(1)
1750         try:
1751             mkfs_fat16(device)
1752         except CriticalException, error:
1753             logging.critical("Execution failed: %s", error)
1754             sys.exit(1)
1755
1756     # check for vfat filesystem
1757     if device is not None and not os.path.isdir(device) and options.syslinux:
1758         try:
1759             check_for_fat(device)
1760         except CriticalException, error:
1761             logging.critical("Execution failed: %s", error)
1762             sys.exit(1)
1763
1764     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1765         print "Warning: the specified device %s does not look like a removable usb device." % device
1766         f = raw_input("Do you really want to continue? y/N ")
1767         if f == "y" or f == "Y":
1768             pass
1769         else:
1770             sys.exit(1)
1771
1772
1773 def handle_compat_warning(device):
1774     """Backwards compatible checks
1775
1776     @device: device that should be checked"""
1777
1778     # make sure we can replace old grml2usb script and warn user when using old way of life:
1779     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1780         print "Warning: the semantics of grml2usb has changed."
1781         print "Instead of using grml2usb /path/to/iso %s you might" % device
1782         print "want to use grml2usb /path/to/iso /dev/... instead."
1783         print "Please check out the grml2usb manpage for details."
1784         f = raw_input("Do you really want to continue? y/N ")
1785         if f == "y" or f == "Y":
1786             pass
1787         else:
1788             sys.exit(1)
1789
1790
1791 def handle_logging():
1792     """Log handling and configuration"""
1793
1794     if options.verbose and options.quiet:
1795         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1796
1797     if options.verbose:
1798         FORMAT = "Debug: %(asctime)-15s %(message)s"
1799         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1800     elif options.quiet:
1801         FORMAT = "Critical: %(message)s"
1802         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1803     else:
1804         FORMAT = "%(message)s"
1805         logging.basicConfig(level=logging.INFO, format=FORMAT)
1806
1807
1808 def handle_bootloader(device):
1809     """wrapper for installing bootloader
1810
1811     @device: device where bootloader should be installed to"""
1812
1813     # Install bootloader only if not using the --copy-only option
1814     if options.copyonly:
1815         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1816     elif os.path.isdir(device):
1817         logging.info("Not installing bootloader as %s is a directory.", device)
1818     else:
1819         install_bootloader(device)
1820
1821
1822 def check_options(opts):
1823     """Check compability of provided user opts
1824
1825     @opts option dict from OptionParser
1826     """
1827     if opts.grubmbr and not opts.grub:
1828         logging.critical("Error: --grub-mbr requires --grub option.")
1829         sys.exit(1)
1830
1831
1832 def check_programs():
1833     """check if all needed programs are installed"""
1834     if options.grub:
1835         if not which("grub-install"):
1836             logging.critical("Fatal: grub-install not available (please install the "
1837                              + "grub package or drop the --grub option)")
1838             sys.exit(1)
1839
1840     if options.syslinux:
1841         if not which("syslinux"):
1842             logging.critical("Fatal: syslinux not available (please install the "
1843                              + "syslinux package or use the --grub option)")
1844             sys.exit(1)
1845
1846     if not which("rsync"):
1847         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1848         sys.exit(1)
1849
1850 def main():
1851     """Main function [make pylint happy :)]"""
1852
1853     if options.version:
1854         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1855         sys.exit(0)
1856
1857     if len(args) < 2:
1858         parser.error("invalid usage")
1859
1860     # log handling
1861     handle_logging()
1862
1863     # make sure we have the appropriate permissions
1864     check_uid_root()
1865
1866     check_options(options)
1867
1868     logging.info("Executing grml2usb version %s", PROG_VERSION)
1869
1870     if options.dryrun:
1871         logging.info("Running in simulation mode as requested via option dry-run.")
1872
1873     check_programs()
1874
1875     # specified arguments
1876     device = args[len(args) - 1]
1877     isos = args[0:len(args) - 1]
1878
1879     if not os.path.isdir(device):
1880         if device[-1:].isdigit():
1881             if int(device[-1:]) > 4 or device[-2:].isdigit():
1882                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1883                 sys.exit(1)
1884
1885     # provide upgrade path
1886     handle_compat_warning(device)
1887
1888     # check for vfat partition
1889     handle_vfat(device)
1890
1891     # main operation (like installing files)
1892     for iso in isos:
1893         install(iso, device)
1894
1895     # install mbr
1896     is_superfloppy = not device[-1:].isdigit()
1897     if is_superfloppy:
1898         logging.info("Detected superfloppy format - not installing MBR")
1899
1900     if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1901         handle_mbr(device)
1902
1903     handle_bootloader(device)
1904
1905     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1906
1907     for flavour in GRML_FLAVOURS:
1908         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1909
1910     # finally be politely :)
1911     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1912
1913
1914 if __name__ == "__main__":
1915     try:
1916         main()
1917     except KeyboardInterrupt:
1918         logging.info("Received KeyboardInterrupt")
1919         cleanup()
1920
1921 ## END OF FILE #################################################################
1922 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8