18463a47fe7555e8174e85b30f2390c6fb768eb2
[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 + 'vmlinuz', iso_mount)
956     initrd = search_file(prefix + 'initrd.img', iso_mount)
957     if kernel is None:
958         # compat for releases < 2011.12
959         kernel = search_file(prefix + 'linux26', iso_mount)
960         initrd = search_file(prefix + 'initrd.gz', iso_mount)
961
962     if kernel is None or initrd is None:
963         logging.critical("Fatal: kernel or initrd not found")
964         raise CriticalException("error locating kernel or initrd file")
965
966     exec_rsync(kernel, release_target + '/')
967     exec_rsync(initrd, release_target + '/')
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|vmlinuz).*)')
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     install_main_config = True
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 re.match(main_identifier, string):
1328             install_main_config = False
1329     if install_main_config:
1330         grub2_config_file = open(grub2_cfg, 'w')
1331         GRML_DEFAULT = grml_flavour
1332         grub2_config_file.write(generate_main_grub2_config(grml_flavour, bootopt))
1333         grub2_config_file.close()
1334
1335     # install flavour specific configuration only *once* as well
1336     grub_flavour_config = True
1337     if os.path.isfile(grub2_cfg):
1338         string = open(grub2_cfg).readlines()
1339         flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1340         for line in string:
1341             if flavour.match(line):
1342                 grub_flavour_config = False
1343
1344     if grub_flavour_config:
1345         grub2_config_file = open(grub2_cfg, 'a')
1346         # display only if the grml flavour isn't the default
1347         if GRML_DEFAULT != grml_flavour:
1348             GRML_FLAVOURS.add(grml_flavour)
1349         grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, bootopt))
1350         grub2_config_file.close()
1351
1352     modify_grub_config(grub2_cfg)
1353
1354
1355 def get_bootoptions(grml_flavour):
1356     """Returns bootoptions for specific flavour
1357
1358     @grml_flavour: name of the grml_flavour
1359     """
1360     # do NOT write "None" in kernel cmdline
1361     if not options.bootoptions:
1362         bootopt = ""
1363     else:
1364         bootopt = " ".join(options.bootoptions)
1365     bootopt = bootopt.replace("%flavour", grml_flavour)
1366     return bootopt
1367
1368
1369 def handle_grub_config(grml_flavour, device, target):
1370     """Main handler for generating grub (v1 and v2) configuration
1371
1372     @grml_flavour: name of grml flavour the configuration should be generated for
1373     @device: device/partition where grub should be installed to
1374     @target: path of grub's configuration files"""
1375
1376     logging.debug("Generating grub configuration")
1377
1378     grub_target = target + '/boot/grub/'
1379
1380     bootopt = get_bootoptions(grml_flavour)
1381
1382     # write grub.cfg
1383     handle_grub2_config(grml_flavour, grub_target, bootopt)
1384
1385
1386 def initial_syslinux_config(target):
1387     """Generates intial syslinux configuration
1388
1389     @target path of syslinux's configuration files"""
1390
1391     target = target + "/"
1392     filename = target + "grmlmain.cfg"
1393     if os.path.isfile(target + "grmlmain.cfg"):
1394         return
1395     data = open(filename, "w")
1396     data.write(generate_main_syslinux_config())
1397     data.close()
1398
1399     filename = target + "hiddens.cfg"
1400     data = open(filename, "w")
1401     data.write("include hidden.cfg\n")
1402     data.close()
1403
1404 def add_entry_if_not_present(filename, entry):
1405     """Write entry into filename if entry is not already in the file
1406
1407     @filanme: name of the file
1408     @entry: data to write to the file
1409     """
1410     data = open(filename, "a+")
1411     for line in data:
1412         if line == entry:
1413             break
1414     else:
1415         data.write(entry)
1416
1417     data.close()
1418
1419 def get_flavour_filename(flavour):
1420     """Generate a iso9960 save filename out of the specified flavour
1421
1422     @flavour: grml flavour
1423     """
1424     return flavour.replace('-', '_')
1425
1426 def adjust_syslinux_bootoptions(src, flavour):
1427     """Adjust existing bootoptions of specified syslinux config to
1428     grml2usb specific ones, e.g. change the location of the kernel...
1429
1430     @src: config file to alter
1431     @flavour: grml flavour
1432     """
1433
1434     append_re = re.compile("^(\s*append.*/boot/release.*)$", re.I)
1435     boot_re = re.compile("/boot/([a-zA-Z0-9_]+/)+([a-zA-Z0-9._]+)")
1436     # flavour_re = re.compile("(label.*)(grml\w+)")
1437     default_re = re.compile("(default.cfg)")
1438     bootid_re = re.compile("bootid=[\w_-]+")
1439     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1440
1441     bootopt = get_bootoptions(flavour)
1442
1443     regexe = []
1444     option_re = None
1445     if options.removeoption:
1446         option_re = re.compile(r'/boot/release/.*/(initrd.gz|initrd.img)')
1447
1448         for regex in options.removeoption:
1449             regexe.append(re.compile(r'%s' % regex))
1450
1451     for line in fileinput.input(src, inplace=1):
1452         line = boot_re.sub(r'/boot/release/%s/\2 ' % flavour.replace('-', ''), line)
1453         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1454         line = default_re.sub(r'%s-\1' % flavour, line)
1455         line = bootid_re.sub('', line)
1456         line = live_media_path_re.sub('', line)
1457         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1458         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1459         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1460         if option_re and option_re.search(line):
1461             for regex in regexe:
1462                 line = regex.sub(' ', line)
1463         sys.stdout.write(line)
1464     fileinput.close()
1465
1466 def adjust_labels(src, replacement):
1467     """Adjust the specified labels in the syslinux config file src with
1468     specified replacement
1469     """
1470     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1471     for line in fileinput.input(src, inplace=1):
1472         line = label_re.sub(replacement, line)
1473         sys.stdout.write(line)
1474     fileinput.close()
1475
1476
1477 def add_syslinux_entry(filename, grml_flavour):
1478     """Add includes for a specific grml_flavour to the specified filename
1479
1480     @filename: syslinux config file
1481     @grml_flavour: grml flavour to add
1482     """
1483
1484     entry_filename = "option_%s.cfg" % grml_flavour
1485     entry = "include %s\n" % entry_filename
1486
1487     add_entry_if_not_present(filename, entry)
1488     path = os.path.dirname(filename)
1489
1490     data = open(path + "/" + entry_filename, "w")
1491     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1492     data.close()
1493
1494 def modify_filenames(grml_flavour, target, filenames):
1495     """Replace the standard filenames with the new ones
1496
1497     @grml_flavour: grml-flavour strin
1498     @target: directory where the files are located
1499     @filenames: list of filenames to alter
1500     """
1501     grml_filename = grml_flavour.replace('-', '_')
1502     for filename in filenames:
1503         old_filename = "%s/%s" % (target, filename)
1504         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1505         os.rename(old_filename, new_filename)
1506
1507
1508 def remove_default_entry(filename):
1509     """Remove the default entry from specified syslinux file
1510
1511     @filename: syslinux config file
1512     """
1513     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1514     for line in fileinput.input(filename, inplace=1):
1515         if default_re.match(line):
1516             continue
1517         sys.stdout.write(line)
1518     fileinput.close()
1519
1520
1521 def handle_syslinux_config(grml_flavour, target):
1522     """Main handler for generating syslinux configuration
1523
1524     @grml_flavour: name of grml flavour the configuration should be generated for
1525     @target: path of syslinux's configuration files"""
1526
1527     logging.debug("Generating syslinux configuration")
1528     syslinux_target = target + '/boot/syslinux/'
1529     # should be present via  copy_bootloader_files(), but make sure it exits:
1530     execute(mkdir, syslinux_target)
1531     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1532
1533
1534     # install main configuration only *once*, no matter how many ISOs we have:
1535     syslinux_config_file = open(syslinux_cfg, 'w')
1536     syslinux_config_file.write("TIMEOUT 300\n")
1537     syslinux_config_file.write("include vesamenu.cfg\n")
1538     syslinux_config_file.close()
1539
1540     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1541     prompt_name.write('menu label S^yslinux prompt\n')
1542     prompt_name.close()
1543
1544     initial_syslinux_config(syslinux_target)
1545     flavour_filename = grml_flavour.replace('-', '_')
1546
1547     if search_file('default.cfg', syslinux_target):
1548         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1549
1550     filename = search_file("new_hidden.cfg", syslinux_target)
1551
1552
1553     # process hidden file
1554     if not search_file("hidden.cfg", syslinux_target):
1555         new_hidden = syslinux_target + "hidden.cfg"
1556         os.rename(filename, new_hidden)
1557         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1558     else:
1559         new_hidden_file =  "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1560         os.rename(filename, new_hidden_file)
1561         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1562         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1563         entry = 'include %s_hidden.cfg\n' % flavour_filename
1564         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1565
1566
1567
1568     new_default = "%s_default.cfg" % (flavour_filename)
1569     entry = 'include %s\n' % new_default
1570     defaults_file = '%s/defaults.cfg' % syslinux_target
1571     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1572     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1573
1574     if os.path.isfile(defaults_file):
1575
1576         # remove default menu entry in menu
1577         remove_default_entry(new_default_with_path)
1578
1579         # adjust all labels for additional isos
1580         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1581         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1582
1583     # always adjust bootoptions
1584     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1585     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1586
1587     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1588
1589     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1590
1591
1592 def handle_bootloader_config(grml_flavour, device, target):
1593     """Main handler for generating bootloader's configuration
1594
1595     @grml_flavour: name of grml flavour the configuration should be generated for
1596     @device: device/partition where bootloader should be installed to
1597     @target: path of bootloader's configuration files"""
1598
1599     global UUID
1600     UUID = get_uuid(target)
1601     if options.skipsyslinuxconfig:
1602         logging.info("Skipping generation of syslinux configuration as requested.")
1603     else:
1604         try:
1605             handle_syslinux_config(grml_flavour, target)
1606         except CriticalException, error:
1607             logging.critical("Fatal: %s", error)
1608             sys.exit(1)
1609
1610     if options.skipgrubconfig:
1611         logging.info("Skipping generation of grub configuration as requested.")
1612     else:
1613         try:
1614             handle_grub_config(grml_flavour, device, target)
1615         except CriticalException, error:
1616             logging.critical("Fatal: %s", error)
1617             sys.exit(1)
1618
1619
1620
1621 def install(image, device):
1622     """Install a grml image to the specified device
1623
1624     @image: directory or is file
1625     @device: partition or directory to install the device
1626     """
1627     iso_mountpoint = image
1628     remove_image_mountpoint = False
1629     if os.path.isdir(image):
1630         logging.info("Using %s as install base", image)
1631     else:
1632         logging.info("Using ISO %s", image)
1633         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1634         register_tmpfile(iso_mountpoint)
1635         remove_image_mountpoint = True
1636         try:
1637             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1638         except CriticalException, error:
1639             logging.critical("Fatal: %s", error)
1640             sys.exit(1)
1641
1642     try:
1643         install_grml(iso_mountpoint, device)
1644     finally:
1645         if remove_image_mountpoint:
1646             try:
1647                 remove_mountpoint(iso_mountpoint)
1648             except CriticalException, error:
1649                 logging.critical("Fatal: %s", error)
1650                 cleanup()
1651
1652
1653
1654 def install_grml(mountpoint, device):
1655     """Main logic for copying files of the currently running grml system.
1656
1657     @mountpoin: directory where currently running live system resides (usually /live/image)
1658     @device: partition where the specified ISO should be installed to"""
1659
1660     device_mountpoint = device
1661     if os.path.isdir(device):
1662         logging.info("Specified device is not a directory, therefore not mounting.")
1663         remove_device_mountpoint = False
1664     else:
1665         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1666         register_tmpfile(device_mountpoint)
1667         remove_device_mountpoint = True
1668         try:
1669             check_for_fat(device)
1670             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1671         except CriticalException, error:
1672             try:
1673                 mount(device, device_mountpoint, "")
1674             except CriticalException, error:
1675                 logging.critical("Fatal: %s", error)
1676                 raise
1677     try:
1678         grml_flavours = identify_grml_flavour(mountpoint)
1679         for flavour in set(grml_flavours):
1680             if not flavour:
1681                 logging.warning("No valid flavour found, please check your iso")
1682             logging.info("Identified grml flavour \"%s\".", flavour)
1683             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1684             GRML_FLAVOURS.add(flavour)
1685     finally:
1686         if remove_device_mountpoint:
1687             remove_mountpoint(device_mountpoint)
1688
1689 def remove_mountpoint(mountpoint):
1690     """remove a registred mountpoint
1691     """
1692
1693     try:
1694         unmount(mountpoint, "")
1695         if os.path.isdir(mountpoint):
1696             os.rmdir(mountpoint)
1697             unregister_tmpfile(mountpoint)
1698     except CriticalException, error:
1699         logging.critical("Fatal: %s", error)
1700         cleanup()
1701
1702 def handle_mbr(device):
1703     """Main handler for installing master boot record (MBR)
1704
1705     @device: device where the MBR should be installed to"""
1706
1707     if options.dryrun:
1708         logging.info("Would install MBR")
1709         return 0
1710
1711     if device[-1:].isdigit():
1712         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1713         partition_number = int(device[-1:]) - 1
1714     else:
1715         logging.warn("Could not detect partition number, not activating partition")
1716         partition_number = None
1717
1718     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1719     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1720     if mbr_device == "/dev/loop":
1721         mbr_device = device
1722         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1723
1724     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1725     if options.syslinuxmbr:
1726         mbrcode = '/usr/lib/syslinux/mbr.bin'
1727     elif options.mbrmenu:
1728         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1729
1730     try:
1731         install_mbr(mbrcode, mbr_device, partition_number, True)
1732     except IOError, error:
1733         logging.critical("Execution failed: %s", error)
1734         sys.exit(1)
1735     except Exception, error:
1736         logging.critical("Execution failed: %s", error)
1737         sys.exit(1)
1738
1739
1740 def handle_vfat(device):
1741     """Check for FAT specific settings and options
1742
1743     @device: device that should checked / formated"""
1744
1745     # make sure we have mkfs.vfat available
1746     if options.fat16:
1747         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1748             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1749             logging.critical('Please make sure to install dosfstools.')
1750             sys.exit(1)
1751
1752         if options.force:
1753             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1754         else:
1755             # make sure the user is aware of what he is doing
1756             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1757             if f == "y" or f == "Y":
1758                 logging.info("Note: you can skip this question using the option --force")
1759             else:
1760                 sys.exit(1)
1761         try:
1762             mkfs_fat16(device)
1763         except CriticalException, error:
1764             logging.critical("Execution failed: %s", error)
1765             sys.exit(1)
1766
1767     # check for vfat filesystem
1768     if device is not None and not os.path.isdir(device) and options.syslinux:
1769         try:
1770             check_for_fat(device)
1771         except CriticalException, error:
1772             logging.critical("Execution failed: %s", error)
1773             sys.exit(1)
1774
1775     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1776         print "Warning: the specified device %s does not look like a removable usb device." % device
1777         f = raw_input("Do you really want to continue? y/N ")
1778         if f == "y" or f == "Y":
1779             pass
1780         else:
1781             sys.exit(1)
1782
1783
1784 def handle_compat_warning(device):
1785     """Backwards compatible checks
1786
1787     @device: device that should be checked"""
1788
1789     # make sure we can replace old grml2usb script and warn user when using old way of life:
1790     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1791         print "Warning: the semantics of grml2usb has changed."
1792         print "Instead of using grml2usb /path/to/iso %s you might" % device
1793         print "want to use grml2usb /path/to/iso /dev/... instead."
1794         print "Please check out the grml2usb manpage for details."
1795         f = raw_input("Do you really want to continue? y/N ")
1796         if f == "y" or f == "Y":
1797             pass
1798         else:
1799             sys.exit(1)
1800
1801
1802 def handle_logging():
1803     """Log handling and configuration"""
1804
1805     if options.verbose and options.quiet:
1806         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1807
1808     if options.verbose:
1809         FORMAT = "Debug: %(asctime)-15s %(message)s"
1810         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1811     elif options.quiet:
1812         FORMAT = "Critical: %(message)s"
1813         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1814     else:
1815         FORMAT = "%(message)s"
1816         logging.basicConfig(level=logging.INFO, format=FORMAT)
1817
1818
1819 def handle_bootloader(device):
1820     """wrapper for installing bootloader
1821
1822     @device: device where bootloader should be installed to"""
1823
1824     # Install bootloader only if not using the --copy-only option
1825     if options.copyonly:
1826         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1827     elif os.path.isdir(device):
1828         logging.info("Not installing bootloader as %s is a directory.", device)
1829     else:
1830         install_bootloader(device)
1831
1832
1833 def check_options(opts):
1834     """Check compability of provided user opts
1835
1836     @opts option dict from OptionParser
1837     """
1838     if opts.grubmbr and not opts.grub:
1839         logging.critical("Error: --grub-mbr requires --grub option.")
1840         sys.exit(1)
1841
1842
1843 def check_programs():
1844     """check if all needed programs are installed"""
1845     if options.grub:
1846         if not which("grub-install"):
1847             logging.critical("Fatal: grub-install not available (please install the "
1848                              + "grub package or drop the --grub option)")
1849             sys.exit(1)
1850
1851     if options.syslinux:
1852         if not which("syslinux"):
1853             logging.critical("Fatal: syslinux not available (please install the "
1854                              + "syslinux package or use the --grub option)")
1855             sys.exit(1)
1856
1857     if not which("rsync"):
1858         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1859         sys.exit(1)
1860
1861 def main():
1862     """Main function [make pylint happy :)]"""
1863
1864     if options.version:
1865         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1866         sys.exit(0)
1867
1868     if len(args) < 2:
1869         parser.error("invalid usage")
1870
1871     # log handling
1872     handle_logging()
1873
1874     # make sure we have the appropriate permissions
1875     check_uid_root()
1876
1877     check_options(options)
1878
1879     logging.info("Executing grml2usb version %s", PROG_VERSION)
1880
1881     if options.dryrun:
1882         logging.info("Running in simulation mode as requested via option dry-run.")
1883
1884     check_programs()
1885
1886     # specified arguments
1887     device = args[len(args) - 1]
1888     isos = args[0:len(args) - 1]
1889
1890     if not os.path.isdir(device):
1891         if device[-1:].isdigit():
1892             if int(device[-1:]) > 4 or device[-2:].isdigit():
1893                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1894                 sys.exit(1)
1895
1896     # provide upgrade path
1897     handle_compat_warning(device)
1898
1899     # check for vfat partition
1900     handle_vfat(device)
1901
1902     # main operation (like installing files)
1903     for iso in isos:
1904         install(iso, device)
1905
1906     # install mbr
1907     is_superfloppy = not device[-1:].isdigit()
1908     if is_superfloppy:
1909         logging.info("Detected superfloppy format - not installing MBR")
1910
1911     if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1912         handle_mbr(device)
1913
1914     handle_bootloader(device)
1915
1916     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1917
1918     for flavour in GRML_FLAVOURS:
1919         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1920
1921     # finally be politely :)
1922     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1923
1924
1925 if __name__ == "__main__":
1926     try:
1927         main()
1928     except KeyboardInterrupt:
1929         logging.info("Received KeyboardInterrupt")
1930         cleanup()
1931
1932 ## END OF FILE #################################################################
1933 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8