2 # -*- coding: utf-8 -*-
7 This script installs a grml system (either a running system or ISO[s]) to a USB device
9 :copyright: (c) 2009 by Michael Prokop <mika@grml.org>
10 :license: GPL v2 or any later version
11 :bugreports: http://grml.org/bugs/
15 from __future__ import with_statement
16 from optparse import OptionParser
17 from inspect import isroutine, isclass
18 import datetime, logging, os, re, subprocess, sys, tempfile, time
21 PROG_VERSION = "0.9.2(pre1)"
22 MOUNTED = set() # register mountpoints
23 TMPFILES = set() # register tmpfiles
24 DATESTAMP = time.mktime(datetime.datetime.now().timetuple()) # unique identifier for syslinux.cfg
27 USAGE = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/sdX#>\n\
29 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
30 Make sure you have at least one grml ISO or a running grml system (/live/image),\n\
31 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
32 and root access. Further information can be found in: man grml2usb"
34 # pylint: disable-msg=C0103
35 parser = OptionParser(usage=USAGE)
36 parser.add_option("--bootoptions", dest="bootoptions",
37 action="store", type="string",
38 help="use specified bootoptions as default")
39 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
40 help="do not copy files but just install a bootloader")
41 parser.add_option("--copy-only", dest="copyonly", action="store_true",
42 help="copy files only but do not install bootloader")
43 parser.add_option("--dry-run", dest="dryrun", action="store_true",
44 help="avoid executing commands")
45 parser.add_option("--fat16", dest="fat16", action="store_true",
46 help="format specified partition with FAT16")
47 parser.add_option("--force", dest="force", action="store_true",
48 help="force any actions requiring manual interaction")
49 parser.add_option("--grub", dest="grub", action="store_true",
50 help="install grub bootloader instead of syslinux")
51 parser.add_option("--initrd", dest="initrd", action="store", type="string",
52 help="install specified initrd instead of the default [TODO]")
53 parser.add_option("--kernel", dest="kernel", action="store", type="string",
54 help="install specified kernel instead of the default [TODO]")
55 parser.add_option("--lilo", dest="lilo", action="store", type="string",
56 help="lilo executable to be used for installing MBR")
57 parser.add_option("--quiet", dest="quiet", action="store_true",
58 help="do not output anything but just errors on console")
59 parser.add_option("--skip-addons", dest="skipaddons", action="store_true",
60 help="do not install /boot/addons/ files")
61 parser.add_option("--skip-mbr", dest="skipmbr", action="store_true",
62 help="do not install a master boot record (MBR) on the device")
63 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
64 help="install specified squashfs file instead of the default [TODO]")
65 parser.add_option("--uninstall", dest="uninstall", action="store_true",
66 help="remove grml ISO files from specified device [TODO]")
67 parser.add_option("--verbose", dest="verbose", action="store_true",
68 help="enable verbose mode")
69 parser.add_option("-v", "--version", dest="version", action="store_true",
70 help="display version and exit")
71 (options, args) = parser.parse_args()
74 class CriticalException(Exception):
75 """Throw critical exception if the exact error is not known but fatal."
77 @Exception: message"""
82 """Cleanup function to make sure there aren't any mounted devices left behind.
85 logging.info("Cleaning up before exiting...")
86 proc = subprocess.Popen(["sync"])
90 for device in MOUNTED:
92 # ignore: RuntimeError: Set changed size during iteration
94 logging.debug('caught expection RuntimeError, ignoring')
97 def register_tmpfile(path):
104 def unregister_tmpfile(path):
109 TMPFILES.remove(path)
112 def register_mountpoint(target):
119 def unregister_mountpoint(target):
123 if target in MOUNTED:
124 MOUNTED.remove(target)
127 def get_function_name(obj):
128 """Helper function for use in execute() to retrive name of a function
130 @obj: the function object
132 if not (isroutine(obj) or isclass(obj)):
134 return obj.__module__ + '.' + obj.__name__
137 def execute(f, *exec_arguments):
138 """Wrapper for executing a command. Either really executes
139 the command (default) or when using --dry-run commandline option
140 just displays what would be executed."""
141 # usage: execute(subprocess.Popen, (["ls", "-la"]))
142 # TODO: doesn't work for proc = execute(subprocess.Popen...() -> any ideas?
144 # pylint: disable-msg=W0141
145 logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, exec_arguments))))
147 # pylint: disable-msg=W0142
148 return f(*exec_arguments)
152 """Check whether a given file can be executed
154 @fpath: full path to file
156 return os.path.exists(fpath) and os.access(fpath, os.X_OK)
160 """Check whether a given program is available in PATH
162 @program: name of executable"""
163 fpath = os.path.split(program)[0]
168 for path in os.environ["PATH"].split(os.pathsep):
169 exe_file = os.path.join(path, program)
176 def search_file(filename, search_path='/bin' + os.pathsep + '/usr/bin'):
177 """Given a search path, find file
179 @filename: name of file to search for
180 @search_path: path where searching for the specified filename"""
182 paths = search_path.split(os.pathsep)
183 current_dir = '' # make pylint happy :)
185 # pylint: disable-msg=W0612
186 for current_dir, directories, files in os.walk(path):
187 if os.path.exists(os.path.join(current_dir, filename)):
191 return os.path.abspath(os.path.join(current_dir, filename))
196 def check_uid_root():
197 """Check for root permissions"""
198 if not os.geteuid()==0:
199 sys.exit("Error: please run this script with uid 0 (root).")
202 def mkfs_fat16(device):
203 """Format specified device with VFAT/FAT16 filesystem.
205 @device: partition that should be formated"""
207 # syslinux -d boot/isolinux /dev/sdb1
208 logging.info("Formating partition with fat16 filesystem")
209 logging.debug("mkfs.vfat -F 16 %s" % device)
210 proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
212 if proc.returncode != 0:
213 raise Exception("error executing mkfs.vfat")
216 def generate_main_grub2_config(grml_flavour, install_partition, bootoptions):
217 """Generate grub2 configuration for use via grub.cfg
221 @grml_flavour: name of grml flavour the configuration should be generated for"""
223 local_datestamp = DATESTAMP
226 ## main grub2 configuration - generated by grml2usb [main config generated at: %(local_datestamp)s]
233 if background_image (hd0, %(install_partition)s)/boot/grub/grml.png ; then
234 set color_normal=black/black
235 set color_highlight=magenta/black
237 set menu_color_normal=cyan/blue
238 set menu_color_highlight=white/blue
241 menuentry "%(grml_flavour)s (default)" {
242 set root=(hd0,%(install_partition)s)
243 linux /boot/release/%(grml_flavour)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_flavour)s %(bootoptions)s
244 initrd /boot/release/%(grml_flavour)s/initrd.gz
247 menuentry "Memory test (memtest86+)" {
248 linux /boot/addons/memtest
251 menuentry "Grub - all in one image" {
252 linux /boot/addons/memdisk
253 initrd /boot/addons/allinone.img
256 menuentry "FreeDOS" {
257 linux /boot/addons/memdisk
258 initrd /boot/addons/balder10.imz
261 #menuentry "Operating System on first partition of first disk" {
266 #menuentry "Operating System on second partition of first disk" {
271 #menuentry "Operating System on first partition of second disk" {
275 #menuentry "Operating System on second partition of second disk" {
280 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp, 'bootoptions': bootoptions, 'install_partition': install_partition } )
283 def generate_flavour_specific_grub2_config(grml_flavour, install_partition, bootoptions):
284 """Generate grub2 configuration for use via grub.cfg
288 @grml_flavour: name of grml flavour the configuration should be generated for"""
290 local_datestamp = DATESTAMP
293 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
294 menuentry "%(grml_flavour)s" {
295 set root=(hd0,%(install_partition)s)
296 linux /boot/release/%(grml_flavour)s/linux26 apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s %(bootoptions)s
297 initrd /boot/release/%(grml_flavour)s/initrd.gz
300 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
301 menuentry "%(grml_flavour)s2ram" {
302 set root=(hd0,%(install_partition)s)
303 linux /boot/release/%(grml_flavour)s/linux26 apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s toram=%(grml_flavour)s %(bootoptions)s
304 initrd /boot/release/%(grml_flavour)s/initrd.gz
306 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
307 menuentry "%(grml_flavour)s-debug" {
308 set root=(hd0,%(install_partition)s)
309 linux /boot/release/%(grml_flavour)s/linux26
310 initrd /boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s debug boot=live initcall_debug%(bootoptions)s
313 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
314 menuentry "%(grml_flavour)s-x" {
315 set root=(hd0,%(install_partition)s)
316 linux /boot/release/%(grml_flavour)s/linux26
317 initrd /boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s startx=wm-ng %(bootoptions)s
320 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
321 menuentry "%(grml_flavour)s-nofb" {
322 set root=(hd0,%(install_partition)s)
323 linux /boot/release/%(grml_flavour)s/linux26
324 initrd /boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal video=ofonly %(bootoptions)s
327 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
328 menuentry "%(grml_flavour)s-failsafe" {
329 set root=(hd0,%(install_partition)s)
330 linux /boot/release/%(grml_flavour)s/linux26
331 initrd /boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal lang=us boot=live noautoconfig atapicd noacpi acpi=off nomodules nofirewire noudev nousb nohotplug noapm nopcmcia maxcpus=1 noscsi noagp nodma ide=nodma noswap nofstab nosound nogpm nosyslog nodhcp nocpu nodisc nomodem xmodule=vesa noraid nolvm %(bootoptions)s
334 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
335 menuentry "%(grml_flavour)s-forensic" {
336 set root=(hd0,%(install_partition)s)
337 linux /boot/release/%(grml_flavour)s/linux26
338 initrd /boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s nofstab noraid nolvm noautoconfig noswap raid=noautodetect %(bootoptions)s
341 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
342 menuentry "%(grml_flavour)s-serial" {
343 set root=(hd0,%(install_partition)s)
344 linux /boot/release/%(grml_flavour)s/linux26
345 initrd /boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal video=vesafb:off console=tty1 console=ttyS0,9600n8 %(bootoptions)s
348 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp, 'bootoptions': bootoptions, 'install_partition': install_partition } )
351 def generate_grub1_config(grml_flavour, install_partition, bootoptions):
352 """Generate grub1 configuration for use via menu.lst
354 @grml_flavour: name of grml flavour the configuration should be generated for"""
356 local_datestamp = DATESTAMP
361 # color red/blue green/black
362 splashimage=/boot/grub/splash.xpm.gz
366 # root=(hd0,%(install_partition)s)
369 title %(grml_flavour)s - Default boot (using 1024x768 framebuffer)
370 kernel /boot/release/%(grml_flavour)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_flavour)s
371 initrd /boot/release/%(grml_flavour)s/initrd.gz
373 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp, 'bootoptions': bootoptions, 'install_partition': install_partition } )
376 def generate_isolinux_splash(grml_flavour):
377 """Generate bootsplash for isolinux/syslinux
379 @grml_flavour: name of grml flavour the configuration should be generated for"""
381 # TODO: adjust last bootsplash line (the one following the "Some information and boot ...")
383 grml_name = grml_flavour
386 \ f17
\f\18/boot/syslinux/logo.16
388 Some information and boot options available via keys F2 - F10. http://grml.org/
390 """ % {'grml_name': grml_name} )
393 def generate_main_syslinux_config(grml_flavour, bootoptions):
394 """Generate main configuration for use in syslinux.cfg
396 @grml_flavour: name of grml flavour the configuration should be generated for
397 @bootoptions: bootoptions that should be used as a default"""
399 local_datestamp = DATESTAMP
402 ## main syslinux configuration - generated by grml2usb [main config generated at: %(local_datestamp)s]
403 # use this to control the bootup via a serial port
408 DISPLAY /boot/syslinux/boot.msg
409 F1 /boot/syslinux/boot.msg
418 F10 /boot/syslinux/f10
419 ## end of main configuration
421 ## global configuration
422 # the default option (using %(grml_flavour)s)
424 KERNEL /boot/release/%(grml_flavour)s/linux26
425 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s %(bootoptions)s
429 KERNEL /boot/addons/memtest
434 KERNEL /boot/addons/memdisk
435 APPEND initrd=/boot/addons/allinone.img
440 KERNEL /boot/addons/memdisk
441 APPEND initrd=/boot/addons/balder10.imz
443 ## end of global configuration
444 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp, 'bootoptions': bootoptions} )
447 def generate_flavour_specific_syslinux_config(grml_flavour, bootoptions):
448 """Generate flavour specific configuration for use in syslinux.cfg
450 @grml_flavour: name of grml flavour the configuration should be generated for
451 @bootoptions: bootoptions that should be used as a default"""
453 local_datestamp = DATESTAMP
457 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
458 LABEL %(grml_flavour)s
459 KERNEL /boot/release/%(grml_flavour)s/linux26
460 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s %(bootoptions)s
462 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
463 LABEL %(grml_flavour)s2ram
464 KERNEL /boot/release/%(grml_flavour)s/linux26
465 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s toram=%(grml_flavour)s %(bootoptions)s
467 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
468 LABEL %(grml_flavour)s-debug
469 KERNEL /boot/release/%(grml_flavour)s/linux26
470 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s debug boot=live initcall_debug%(bootoptions)s
472 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
473 LABEL %(grml_flavour)s-x
474 KERNEL /boot/release/%(grml_flavour)s/linux26
475 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s startx=wm-ng %(bootoptions)s
477 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
478 LABEL %(grml_flavour)s-nofb
479 KERNEL /boot/release/%(grml_flavour)s/linux26
480 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal video=ofonly %(bootoptions)s
482 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
483 LABEL %(grml_flavour)s-failsafe
484 KERNEL /boot/release/%(grml_flavour)s/linux26
485 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal lang=us boot=live noautoconfig atapicd noacpi acpi=off nomodules nofirewire noudev nousb nohotplug noapm nopcmcia maxcpus=1 noscsi noagp nodma ide=nodma noswap nofstab nosound nogpm nosyslog nodhcp nocpu nodisc nomodem xmodule=vesa noraid nolvm %(bootoptions)s
487 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
488 LABEL %(grml_flavour)s-forensic
489 KERNEL /boot/release/%(grml_flavour)s/linux26
490 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce vga=791 quiet module=%(grml_flavour)s nofstab noraid nolvm noautoconfig noswap raid=noautodetect %(bootoptions)s
492 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
493 LABEL %(grml_flavour)s-serial
494 KERNEL /boot/release/%(grml_flavour)s/linux26
495 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal video=vesafb:off console=tty1 console=ttyS0,9600n8 %(bootoptions)s
496 """ % {'grml_flavour': grml_flavour, 'local_datestamp': local_datestamp, 'bootoptions': bootoptions} )
499 def install_grub(device):
500 """Install grub on specified device.
502 @mntpoint: mountpoint of device where grub should install its files to
503 @device: partition where grub should be installed to"""
506 logging.info("Would execute grub-install [--root-directory=mount_point] %s now.", device)
508 device_mountpoint = tempfile.mkdtemp()
509 register_tmpfile(device_mountpoint)
511 mount(device, device_mountpoint, "")
512 logging.debug("grub-install --root-directory=%s %s", device_mountpoint, device)
513 proc = subprocess.Popen(["grub-install", "--root-directory=%s" % device_mountpoint, device], stdout=file(os.devnull, "r+"))
515 if proc.returncode != 0:
516 raise Exception("error executing grub-install")
517 except CriticalException, error:
518 logging.critical("Fatal: %s" % error)
523 unmount(device_mountpoint, "")
524 os.rmdir(device_mountpoint)
525 unregister_tmpfile(device_mountpoint)
528 def install_syslinux(device):
529 """Install syslinux on specified device.
531 @device: partition where syslinux should be installed to"""
534 logging.info("Would install syslinux as bootloader on %s", device)
537 # syslinux -d boot/isolinux /dev/sdb1
538 logging.info("Installing syslinux as bootloader")
539 logging.debug("syslinux -d boot/syslinux %s" % device)
540 proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
542 if proc.returncode != 0:
543 raise CriticalException("Error executing syslinux (either try --fat16 or --grub?)")
546 def install_bootloader(device):
547 """Install bootloader on specified device.
549 @device: partition where bootloader should be installed to"""
551 # Install bootloader on the device (/dev/sda),
552 # not on the partition itself (/dev/sda1)?
553 #if partition[-1:].isdigit():
554 # device = re.match(r'(.*?)\d*$', partition).group(1)
562 install_syslinux(device)
563 except CriticalException, error:
564 logging.critical("Fatal: %s" % error)
569 def install_lilo_mbr(lilo, device):
572 # to support -A for extended partitions:
573 logging.info("Installing MBR")
574 logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
575 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
577 if proc.returncode != 0:
578 raise Exception("error executing lilo")
580 # activate partition:
581 logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
582 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
584 if proc.returncode != 0:
585 raise Exception("error executing lilo")
588 def install_syslinux_mbr(device):
591 # lilo's mbr is broken, use the one from syslinux instead:
592 if not os.path.isfile("/usr/lib/syslinux/mbr.bin"):
593 raise Exception("/usr/lib/syslinux/mbr.bin can not be read")
595 logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
597 # TODO -> use Popen instead?
598 retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
600 logging.critical("Error copying MBR to device (%s)" % retcode)
601 except OSError, error:
602 logging.critical("Execution failed:", error)
605 def InstallMBR(mbrtemplate, device, partition, ismirbsdmbr=True):
606 """Installs an MBR to a device.
608 Retrieve the partition table from "device", install an MBR from
609 the "mbrtemplate" file, set the "partition" (0..3) active, and
610 install the result back to "device".
612 "device" may be the name of a file assumed to be a hard disc
613 (or USB stick) image, or something like "/dev/sdb". "partition"
614 must be a number between 0 and 3, inclusive. "mbrtemplate" must
615 be a valid MBR file of at least 440 (439 if ismirbsdmbr) bytes.
617 If "ismirbsdmbr", the partitions' active flags are not changed.
618 Instead, the MBR's default value is set accordingly.
621 if (partition < 0) or (partition > 3):
622 raise ValueError("partition must be between 0 and 3")
629 tmpf = tempfile.NamedTemporaryFile()
631 logging.debug("executing: dd if='%s' of='%s' bs=512 count=1" % (device, tmpf.name))
632 proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"], stderr=file(os.devnull, "r+"))
634 if proc.returncode != 0:
635 raise Exception("error executing dd (first run)")
636 # os.system("dd if='%s' of='%s' bs=512 count=1" % (device, tmpf.name))
638 logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc" % (mbrtemplate, tmpf.name, nmbrbytes))
639 proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes, "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
641 if proc.returncode != 0:
642 raise Exception("error executing dd (second run)")
643 # os.system("dd if='%s' of='%s' bs=%d count=1 conv=notrunc" % (mbrtemplate, tmpf.name, nmbrbytes))
645 mbrcode = tmpf.file.read(512)
646 if len(mbrcode) < 512:
647 raise EOFError("MBR size (%d) < 512" % len(mbrcode))
650 mbrcode = mbrcode[0:439] + chr(partition) + \
651 mbrcode[440:510] + "\x55\xAA"
653 actives = ["\x00", "\x00", "\x00", "\x00"]
654 actives[partition] = "\x80"
655 mbrcode = mbrcode[0:446] + actives[0] + \
656 mbrcode[447:462] + actives[1] + \
657 mbrcode[463:478] + actives[2] + \
658 mbrcode[479:494] + actives[3] + \
659 mbrcode[495:510] + "\x55\xAA"
663 tmpf.file.write(mbrcode)
666 #os.system("dd if='%s' of='%s' bs=512 count=1 conv=notrunc" % (tmpf.name, device))
667 logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc" % (tmpf.name, "/tmp/mbr"))
668 proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % "/tmp/mbr", "bs=512", "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
670 if proc.returncode != 0:
671 raise Exception("error executing dd (third run)")
672 # os.system("dd if='%s' of='%s' bs=512 count=1 conv=notrunc" % (tmpf.name, "/tmp/mbr"))
675 def install_mbr(device):
676 """Install a default master boot record on given device
678 @device: device where MBR should be installed to"""
680 if not is_writeable(device):
681 raise IOError("device not writeable for user")
683 # try to use system's lilo
687 # otherwise fall back to our static version
688 from platform import architecture
689 if architecture()[0] == '64bit':
690 lilo = '/usr/share/grml2usb/lilo/lilo.static.amd64'
692 lilo = '/usr/share/grml2usb/lilo/lilo.static.i386'
693 # finally prefer a specified lilo executable
698 raise Exception("lilo executable can not be execute")
701 logging.info("Would install MBR running lilo and using syslinux.")
704 install_lilo_mbr(lilo, device)
705 install_syslinux_mbr(device)
708 def is_writeable(device):
709 """Check if the device is writeable for the current user
711 @device: partition where bootloader should be installed to"""
715 #raise Exception("no device for checking write permissions")
717 if not os.path.exists(device):
720 return os.access(device, os.W_OK) and os.access(device, os.R_OK)
723 def mount(source, target, mount_options):
724 """Mount specified source on given target
726 @source: name of device/ISO that should be mounted
727 @target: directory where the ISO should be mounted to
728 @options: mount specific options"""
730 # note: options.dryrun does not work here, as we have to
731 # locate files and identify the grml flavour
732 logging.debug("mount %s %s %s" % (mount_options, source, target))
733 proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
735 if proc.returncode != 0:
736 raise CriticalException("Error executing mount")
738 logging.debug("register_mountpoint(%s)" % target)
739 register_mountpoint(target)
742 def unmount(target, unmount_options):
743 """Unmount specified target
745 @target: target where something is mounted on and which should be unmounted
746 @options: options for umount command"""
748 # make sure we unmount only already mounted targets
749 target_unmount = False
750 mounts = open('/proc/mounts').readlines()
751 mountstring = re.compile(".*%s.*" % re.escape(target))
753 if re.match(mountstring, line):
754 target_unmount = True
756 if not target_unmount:
757 logging.debug("%s not mounted anymore" % target)
759 logging.debug("umount %s %s" % (list(unmount_options), target))
760 proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
762 if proc.returncode != 0:
763 raise Exception("Error executing umount")
765 logging.debug("unregister_mountpoint(%s)" % target)
766 unregister_mountpoint(target)
769 def check_for_usbdevice(device):
770 """Check whether the specified device is a removable USB device
772 @device: device name, like /dev/sda1 or /dev/sda
775 usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
776 usbdevice = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
777 if os.path.isfile(usbdevice):
778 is_usb = open(usbdevice).readline()
785 def check_for_fat(partition):
786 """Check whether specified partition is a valid VFAT/FAT16 filesystem
788 @partition: device name of partition"""
791 udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t", partition],
792 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
793 filesystem = udev_info.communicate()[0].rstrip()
795 if udev_info.returncode == 2:
796 raise CriticalException("Failed to read device %s"
797 " (wrong UID/permissions or device not present?)" % partition)
799 if filesystem != "vfat":
800 raise CriticalException("Device %s does not contain a FAT16 partition." % partition)
803 raise CriticalException("Sorry, /lib/udev/vol_id not available.")
806 def mkdir(directory):
807 """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
809 # just silently pass as it's just fine it the directory exists
810 if not os.path.isdir(directory):
812 os.makedirs(directory)
813 # pylint: disable-msg=W0704
818 def copy_system_files(grml_flavour, iso_mount, target):
821 squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
823 logging.critical("Fatal: squashfs file not found")
825 squashfs_target = target + '/live/'
826 execute(mkdir, squashfs_target)
827 # use install(1) for now to make sure we can write the files afterwards as normal user as well
828 logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
829 proc = subprocess.Popen(["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
832 filesystem_module = search_file('filesystem.module', iso_mount)
833 if filesystem_module is None:
834 logging.critical("Fatal: filesystem.module not found")
836 logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
837 proc = subprocess.Popen(["install", "--mode=664", filesystem_module,
838 squashfs_target + grml_flavour + '.module'])
841 release_target = target + '/boot/release/' + grml_flavour
842 execute(mkdir, release_target)
844 kernel = search_file('linux26', iso_mount)
846 logging.critical("Fatal kernel not found")
848 logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
849 proc = subprocess.Popen(["install", "--mode=664", kernel, release_target + '/linux26'])
852 initrd = search_file('initrd.gz', iso_mount)
854 logging.critical("Fatal: initrd not found")
856 logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
857 proc = subprocess.Popen(["install", "--mode=664", initrd, release_target + '/initrd.gz'])
861 def copy_grml_files(iso_mount, target):
864 grml_target = target + '/grml/'
865 execute(mkdir, grml_target)
867 for myfile in 'grml-cheatcodes.txt', 'grml-version', 'LICENSE.txt', 'md5sums', 'README.txt':
868 grml_file = search_file(myfile, iso_mount)
869 if grml_file is None:
870 logging.warn("Warning: myfile %s could not be found - can not install it", myfile)
872 logging.debug("cp %s %s" % (grml_file, grml_target + grml_file))
873 proc = subprocess.Popen(["install", "--mode=664", grml_file, grml_target + myfile])
876 grml_web_target = grml_target + '/web/'
877 execute(mkdir, grml_web_target)
879 for myfile in 'index.html', 'style.css':
880 grml_file = search_file(myfile, iso_mount)
881 if grml_file is None:
882 logging.warn("Warning: myfile %s could not be found - can not install it")
884 logging.debug("cp %s %s" % (grml_file, grml_web_target + grml_file))
885 proc = subprocess.Popen(["install", "--mode=664", grml_file, grml_web_target + myfile])
888 grml_webimg_target = grml_web_target + '/images/'
889 execute(mkdir, grml_webimg_target)
891 for myfile in 'button.png', 'favicon.png', 'linux.jpg', 'logo.png':
892 grml_file = search_file(myfile, iso_mount)
893 if grml_file is None:
894 logging.warn("Warning: myfile %s could not be found - can not install it")
896 logging.debug("cp %s %s" % (grml_file, grml_webimg_target + grml_file))
897 proc = subprocess.Popen(["install", "--mode=664", grml_file, grml_webimg_target + myfile])
901 def copy_addons(iso_mount, target):
903 addons = target + '/boot/addons/'
904 execute(mkdir, addons)
906 # grub all-in-one image
907 allinoneimg = search_file('allinone.img', iso_mount)
908 if allinoneimg is None:
909 logging.warn("Warning: allinone.img not found - can not install it")
911 logging.debug("cp %s %s" % (allinoneimg, addons + '/allinone.img'))
912 proc = subprocess.Popen(["install", "--mode=664", allinoneimg, addons + 'allinone.img'])
916 balderimg = search_file('balder10.imz', iso_mount)
917 if balderimg is None:
918 logging.warn("Warning: balder10.imz not found - can not install it")
920 logging.debug("cp %s %s" % (balderimg, addons + '/balder10.imz'))
921 proc = subprocess.Popen(["install", "--mode=664", balderimg, addons + 'balder10.imz'])
925 memdiskimg = search_file('memdisk', iso_mount)
926 if memdiskimg is None:
927 logging.warn("Warning: memdisk not found - can not install it")
929 logging.debug("cp %s %s" % (memdiskimg, addons + '/memdisk'))
930 proc = subprocess.Popen(["install", "--mode=664", memdiskimg, addons + 'memdisk'])
934 memtestimg = search_file('memtest', iso_mount)
935 if memtestimg is None:
936 logging.warn("Warning: memtest not found - can not install it")
938 logging.debug("cp %s %s" % (memtestimg, addons + '/memtest'))
939 proc = subprocess.Popen(["install", "--mode=664", memtestimg, addons + 'memtest'])
943 def copy_bootloader_files(iso_mount, target):
946 syslinux_target = target + '/boot/syslinux/'
947 execute(mkdir, syslinux_target)
949 logo = search_file('logo.16', iso_mount)
950 logging.debug("cp %s %s" % (logo, syslinux_target + 'logo.16'))
951 proc = subprocess.Popen(["install", "--mode=664", logo, syslinux_target + 'logo.16'])
954 for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
955 bootsplash = search_file(ffile, iso_mount)
956 logging.debug("cp %s %s" % (bootsplash, syslinux_target + ffile))
957 proc = subprocess.Popen(["install", "--mode=664", bootsplash, syslinux_target + ffile])
960 grub_target = target + '/boot/grub/'
961 execute(mkdir, grub_target)
963 if not os.path.isfile("/usr/share/grml2usb/grub/splash.xpm.gz"):
964 logging.critical("Error: /usr/share/grml2usb/grub/splash.xpm.gz can not be read.")
967 logging.debug("cp /usr/share/grml2usb/grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz')
968 proc = subprocess.Popen(["install", "--mode=664", '/usr/share/grml2usb/grub/splash.xpm.gz',
969 grub_target + 'splash.xpm.gz'])
972 if not os.path.isfile("/usr/share/grml2usb/grub/stage2_eltorito"):
973 logging.critical("Error: /usr/share/grml2usb/grub/stage2_eltorito can not be read.")
976 logging.debug("cp /usr/share/grml2usb/grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito')
977 proc = subprocess.Popen(["install", "--mode=664", '/usr/share/grml2usb/grub/stage2_eltorito',
978 grub_target + 'stage2_eltorito'])
982 def install_iso_files(grml_flavour, iso_mount, device, target):
983 """Copy files from ISO on given target"""
986 # * make sure grml_flavour, iso_mount, target are set when the function is called, otherwise raise exception
987 # * provide alternative search_file() if file information is stored in a config.ini file?
988 # * catch "install: .. No space left on device" & CO
991 logging.info("Would copy files to %s", iso_mount)
993 elif not options.bootloaderonly:
994 logging.info("Copying files. This might take a while....")
995 copy_system_files(grml_flavour, iso_mount, target)
996 copy_grml_files(iso_mount, target)
998 if not options.skipaddons:
999 copy_addons(iso_mount, target)
1001 if not options.copyonly:
1002 copy_bootloader_files(iso_mount, target)
1004 if not options.dryrun:
1005 handle_bootloader_config(grml_flavour, device, target)
1007 # make sure we sync filesystems before returning
1008 proc = subprocess.Popen(["sync"])
1012 def uninstall_files(device):
1013 """Get rid of all grml files on specified device
1015 @device: partition where grml2usb files should be removed from"""
1018 logging.critical("TODO: uninstalling files from %s not yet implement, sorry." % device)
1021 def identify_grml_flavour(mountpath):
1022 """Get name of grml flavour
1024 @mountpath: path where the grml ISO is mounted to
1025 @return: name of grml-flavour"""
1027 version_file = search_file('grml-version', mountpath)
1029 if version_file == "":
1030 logging.critical("Error: could not find grml-version file.")
1034 tmpfile = open(version_file, 'r')
1035 grml_info = tmpfile.readline()
1036 grml_flavour = re.match(r'[\w-]*', grml_info).group()
1040 logging.critical("Unexpected error:", sys.exc_info()[0])
1045 def handle_grub_config(grml_flavour, device, target):
1048 logging.debug("Generating grub configuration")
1049 #with open("...", "w") as f:
1050 #f.write("bla bla bal")
1052 grub_target = target + '/boot/grub/'
1053 # should be present via copy_bootloader_files(), but make sure it exists:
1054 execute(mkdir, grub_target)
1055 # we have to adjust root() inside grub configuration
1056 if device[-1:].isdigit():
1057 install_partition = device[-1:]
1060 #logging.debug("Creating grub1 configuration file")
1061 #grub_config_file = open(grub_target + 'menu.lst', 'w')
1062 #grub_config_file.write(generate_grub1_config(grml_flavour, install_partition, options.bootoptions))
1063 #grub_config_file.close()
1064 # TODO => generate_main_grub1_config() && generate_flavour_specific_grub1_config()
1067 grub2_cfg = grub_target + 'grub.cfg'
1068 logging.debug("Creating grub2 configuration file")
1070 # install main configuration only *once*, no matter how many ISOs we have:
1071 if os.path.isfile(grub2_cfg):
1072 string = open(grub2_cfg).readline()
1073 main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1074 if not re.match(main_identifier, string):
1075 grub2_config_file = open(grub2_cfg, 'w')
1076 logging.info("Note: grml flavour %s is being installed as the default booting system." % grml_flavour)
1077 grub2_config_file.write(generate_main_grub2_config(grml_flavour, install_partition, options.bootoptions))
1078 grub2_config_file.close()
1080 grub2_config_file = open(grub2_cfg, 'w')
1081 grub2_config_file.write(generate_main_grub2_config(grml_flavour, install_partition, options.bootoptions))
1082 grub2_config_file.close()
1084 grub_flavour_config = True
1085 if os.path.isfile(grub2_cfg):
1086 string = open(grub2_cfg).readlines()
1087 logging.info("Note: you can boot flavour %s using '%s' on the commandline." % (grml_flavour, grml_flavour))
1088 flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1090 if flavour.match(line):
1091 grub_flavour_config = False
1093 if grub_flavour_config:
1094 grub2_config_file = open(grub2_cfg, 'a')
1095 grub2_config_file.write(generate_flavour_specific_grub2_config(grml_flavour, install_partition, options.bootoptions))
1096 grub2_config_file.close( )
1099 def handle_syslinux_config(grml_flavour, target):
1103 logging.info("Generating syslinux configuration")
1104 syslinux_target = target + '/boot/syslinux/'
1105 # should be present via copy_bootloader_files(), but make sure it exits:
1106 execute(mkdir, syslinux_target)
1107 syslinux_cfg = syslinux_target + 'syslinux.cfg'
1109 # install main configuration only *once*, no matter how many ISOs we have:
1110 if os.path.isfile(syslinux_cfg):
1111 string = open(syslinux_cfg).readline()
1112 main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(DATESTAMP)))
1113 if not re.match(main_identifier, string):
1114 syslinux_config_file = open(syslinux_cfg, 'w')
1115 logging.info("Note: grml flavour %s is being installed as the default booting system." % grml_flavour)
1116 syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
1117 syslinux_config_file.close()
1119 syslinux_config_file = open(syslinux_cfg, 'w')
1120 syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
1121 syslinux_config_file.close()
1123 # install flavour specific configuration only *once* as well
1124 # kind of ugly - I'm pretty sure this could be smoother...
1125 syslinux_flavour_config = True
1126 if os.path.isfile(syslinux_cfg):
1127 string = open(syslinux_cfg).readlines()
1128 logging.info("Note: you can boot flavour %s using '%s' on the commandline." % (grml_flavour, grml_flavour))
1129 flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(DATESTAMP))))
1131 if flavour.match(line):
1132 syslinux_flavour_config = False
1134 if syslinux_flavour_config:
1135 syslinux_config_file = open(syslinux_cfg, 'a')
1136 syslinux_config_file.write(generate_flavour_specific_syslinux_config(grml_flavour, options.bootoptions))
1137 syslinux_config_file.close( )
1139 logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
1140 isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
1141 isolinux_splash.write(generate_isolinux_splash(grml_flavour))
1142 isolinux_splash.close( )
1145 def handle_bootloader_config(grml_flavour, device, target):
1149 handle_grub_config(grml_flavour, device, target)
1151 handle_syslinux_config(grml_flavour, target)
1154 def handle_iso(iso, device):
1155 """Main logic for mounting ISOs and copying files.
1157 @iso: full path to the ISO that should be installed to the specified device
1158 @device: partition where the specified ISO should be installed to"""
1160 logging.info("Using ISO %s" % iso)
1162 if os.path.isdir(iso):
1163 logging.critical("TODO: /live/image handling not yet implemented - sorry") # TODO
1166 iso_mountpoint = tempfile.mkdtemp()
1167 register_tmpfile(iso_mountpoint)
1168 remove_iso_mountpoint = True
1170 if not os.path.isfile(iso):
1171 logging.critical("Fatal: specified ISO %s could not be read" % iso)
1176 mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
1177 except CriticalException, error:
1178 logging.critical("Fatal: %s" % error)
1181 if os.path.isdir(device):
1182 logging.info("Specified target is a directory, not mounting therefor.")
1183 device_mountpoint = device
1184 remove_device_mountpoint = False
1187 device_mountpoint = tempfile.mkdtemp()
1188 register_tmpfile(device_mountpoint)
1189 remove_device_mountpoint = True
1191 mount(device, device_mountpoint, "")
1192 except CriticalException, error:
1193 logging.critical("Fatal: %s" % error)
1198 grml_flavour = identify_grml_flavour(iso_mountpoint)
1199 logging.info("Identified grml flavour \"%s\"." % grml_flavour)
1200 install_iso_files(grml_flavour, iso_mountpoint, device, device_mountpoint)
1202 logging.critical("Fatal: a critical error happend during execution (not a grml ISO?), giving up")
1205 if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
1206 unmount(iso_mountpoint, "")
1207 os.rmdir(iso_mountpoint)
1208 unregister_tmpfile(iso_mountpoint)
1209 if remove_device_mountpoint:
1210 unmount(device_mountpoint, "")
1211 if os.path.isdir(device_mountpoint):
1212 os.rmdir(device_mountpoint)
1213 unregister_tmpfile(device_mountpoint)
1216 def handle_mbr(device):
1220 # if not options.mbr:
1221 # logging.info("You are NOT using the --mbr option. Consider using it if your device does not boot.")
1223 # make sure we install MBR on /dev/sdX and not /dev/sdX#
1225 # make sure we have syslinux available
1227 if not options.skipmbr:
1228 if not which("syslinux") and not options.copyonly and not options.dryrun:
1229 logging.critical('Sorry, syslinux not available. Exiting.')
1230 logging.critical('Please install syslinux or consider using the --grub option.')
1233 if not options.skipmbr:
1234 if device[-1:].isdigit():
1235 mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1236 partition_number = int(device[-1:]) - 1
1239 # install_mbr(mbr_device)
1240 InstallMBR('/usr/share/grml2usb/mbr/mbrmgr', mbr_device, partition_number, True)
1241 except IOError, error:
1242 logging.critical("Execution failed: %s", error)
1244 except Exception, error:
1245 logging.critical("Execution failed: %s", error)
1249 def handle_vfat(device):
1252 # make sure we have mkfs.vfat available
1253 if options.fat16 and not options.force:
1254 if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1255 logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1256 logging.critical('Please make sure to install dosfstools.')
1259 # make sure the user is aware of what he is doing
1260 f = raw_input("Are you sure you want to format the device with a fat16 filesystem? y/N ")
1261 if f == "y" or f == "Y":
1262 logging.info("Note: you can skip this question using the option --force")
1267 # check for vfat filesystem
1268 if device is not None and not os.path.isdir(device):
1270 check_for_fat(device)
1271 except CriticalException, error:
1272 logging.critical("Execution failed: %s", error)
1275 if not check_for_usbdevice(device):
1276 print "Warning: the specified device %s does not look like a removable usb device." % device
1277 f = raw_input("Do you really want to continue? y/N ")
1278 if f == "y" or f == "Y":
1284 def handle_compat_warning(device):
1287 # make sure we can replace old grml2usb script and warn user when using old way of life:
1288 if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1289 print "Warning: the semantics of grml2usb has changed."
1290 print "Instead of using grml2usb /path/to/iso %s you might" % device
1291 print "want to use grml2usb /path/to/iso /dev/... instead."
1292 print "Please check out the grml2usb manpage for details."
1293 f = raw_input("Do you really want to continue? y/N ")
1294 if f == "y" or f == "Y":
1300 def handle_logging():
1304 FORMAT = "%(asctime)-15s %(message)s"
1305 logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1307 FORMAT = "Critial: %(message)s"
1308 logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1310 FORMAT = "Info: %(message)s"
1311 logging.basicConfig(level=logging.INFO, format=FORMAT)
1314 def handle_bootloader(device):
1316 # Install bootloader only if not using the --copy-only option
1317 if options.copyonly:
1318 logging.info("Not installing bootloader and its files as requested via option copyonly.")
1320 install_bootloader(device)
1324 """Main function [make pylint happy :)]"""
1327 print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1331 parser.error("invalid usage")
1336 # make sure we have the appropriate permissions
1340 logging.info("Running in simulate mode as requested via option dry-run.")
1342 # specified arguments
1343 device = args[len(args) - 1]
1344 isos = args[0:len(args) - 1]
1346 if device[-1:].isdigit():
1347 if int(device[-1:]) > 4:
1348 logging.critical("Fatal: installation on partition number >4 not supported. (As the BIOS won't support it.)")
1351 logging.critical("Fatal: installation on raw device not supported. (As the BIOS won't support it.)")
1354 # provide upgrade path
1355 handle_compat_warning(device)
1357 # check for vfat partition
1360 # main operation (like installing files)
1362 handle_iso(iso, device)
1367 handle_bootloader(device)
1369 # finally be politely :)
1370 logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
1373 if __name__ == "__main__":
1376 except KeyboardInterrupt:
1377 logging.info("Received KeyboardInterrupt")
1380 ## END OF FILE #################################################################
1381 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8