2 # -*- coding: utf-8 -*-
7 This script installs a grml system (running system / 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/
16 * detect old grml2usb usage and inform user about it and exit then
17 -> rename grml2usb.py to grml2usb then
18 * handling of bootloader configuration for multiple ISOs
19 * verify that the specified device is really USB (/dev/usb-sd* -> /sys/devices/*/removable_)
20 * validate partition schema? bootable flag
21 * implement missing options (--kernel, --initrd, --uninstall,...)
22 * improve error handling :)
23 * get rid of "if not dry_run" inside code/functions
24 * implement mount handling
25 * implement logic for storing information about copied files
26 -> register every single file?
27 * trap handling (like unmount devices when interrupting?)
28 * get rid of all TODOs in code :)
29 * graphical version? :)
32 from __future__ import with_statement
33 import os, re, subprocess, sys, tempfile
34 from optparse import OptionParser
35 from os.path import exists, join, abspath
36 from os import pathsep
37 from inspect import isroutine, isclass
40 PROG_VERSION = "0.0.1"
42 skip_mbr = False # By default we don't want to skip it; TODO - can we get rid of that?
45 usage = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/ice>\n\
47 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
48 Make sure you have at least a grml ISO or a running grml system (/live/image),\n\
49 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
52 parser = OptionParser(usage=usage)
53 parser.add_option("--bootoptions", dest="bootoptions",
54 action="store", type="string",
55 help="use specified bootoptions as defaut")
56 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
57 help="do not copy files only but just install a bootloader")
58 parser.add_option("--copy-only", dest="copyonly", action="store_true",
59 help="copy files only and do not install bootloader")
60 parser.add_option("--dry-run", dest="dryrun", action="store_true",
61 help="do not actually execute any commands")
62 parser.add_option("--fat16", dest="fat16", action="store_true",
63 help="format specified partition with FAT16")
64 parser.add_option("--force", dest="force", action="store_true",
65 help="force any actions requiring manual interaction")
66 parser.add_option("--grub", dest="grub", action="store_true",
67 help="install grub bootloader instead of syslinux")
68 parser.add_option("--initrd", dest="initrd", action="store", type="string",
69 help="install specified initrd instead of the default")
70 parser.add_option("--kernel", dest="kernel", action="store", type="string",
71 help="install specified kernel instead of the default")
72 parser.add_option("--mbr", dest="mbr", action="store_true",
73 help="install master boot record (MBR) on the device")
74 parser.add_option("--mountpath", dest="mountpath", action="store_true",
75 help="install system to specified mount path")
76 parser.add_option("--quiet", dest="quiet", action="store_true",
77 help="do not output anything than errors on console")
78 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
79 help="install specified squashfs file instead of the default")
80 parser.add_option("--uninstall", dest="uninstall", action="store_true",
81 help="remove grml ISO files")
82 parser.add_option("--verbose", dest="verbose", action="store_true",
83 help="enable verbose mode")
84 parser.add_option("-v", "--version", dest="version", action="store_true",
85 help="display version and exit")
86 (options, args) = parser.parse_args()
89 def get_function_name(obj):
90 if not (isroutine(obj) or isclass(obj)):
92 return obj.__module__ + '.' + obj.__name__
94 def execute(f, *args):
95 """Wrapper for executing a command. Either really executes
96 the command (default) or when using --dry-run commandline option
97 just displays what would be executed."""
98 # demo: execute(subprocess.Popen, (["ls", "-la"]))
100 logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, args))))
106 """Check whether a given file can be executed
108 @fpath: full path to file
110 return os.path.exists(fpath) and os.access(fpath, os.X_OK)
114 """Check whether a given program is available in PATH
116 @program: name of executable"""
117 fpath, fname = os.path.split(program)
122 for path in os.environ["PATH"].split(os.pathsep):
123 exe_file = os.path.join(path, program)
130 def search_file(filename, search_path='/bin' + pathsep + '/usr/bin'):
131 """Given a search path, find file"""
133 paths = search_path.split(pathsep)
135 for current_dir, directories, files in os.walk(path):
136 if exists(join(current_dir, filename)):
140 return abspath(join(current_dir, filename))
145 def check_uid_root():
146 """Check for root permissions"""
147 if not os.geteuid()==0:
148 sys.exit("Error: please run this script with uid 0 (root).")
151 def install_syslinux(device, dry_run=False):
153 """Install syslinux on specified device."""
154 logging.critical("debug: syslinux %s [TODO]" % device)
156 # syslinux -d boot/isolinux /dev/usb-sdb1
159 def generate_grub_config(grml_flavour):
160 """Generate grub configuration for use via menu,lst"""
163 # * install main part of configuration just *once* and append
164 # flavour specific configuration only
165 # * what about systems using grub2 without having grub1 available?
168 grml_name = grml_flavour
173 # color red/blue green/black
174 splashimage=/boot/grub/splash.xpm.gz
179 title %(grml_name)s - Default boot (using 1024x768 framebuffer)
180 kernel /boot/release/%(grml_name)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_name)s
181 initrd /boot/release/%(grml_name)s/initrd.gz
183 # TODO: extend configuration :)
187 def generate_isolinux_splash(grml_flavour):
188 """Generate bootsplash for isolinux/syslinux"""
191 # * adjust last bootsplash line
193 grml_name = grml_flavour
196 \ f17
\f\18/boot/isolinux/logo.16
198 Some information and boot options available via keys F2 - F10. http://grml.org/
202 def generate_syslinux_config(grml_flavour):
203 """Generate configuration for use in syslinux.cfg"""
206 # * install main part of configuration just *once* and append
207 # flavour specific configuration only
208 # * unify isolinux and syslinux setup ("INCLUDE /boot/...")
211 grml_name = grml_flavour
214 # use this to control the bootup via a serial port
219 DISPLAY /boot/isolinux/boot.msg
220 F1 /boot/isolinux/boot.msg
229 F10 /boot/isolinux/f10
232 KERNEL /boot/release/%(grml_name)s/linux26
233 APPEND initrd=/boot/release/%(grml_name)s/initrd.gz apm=power-off lang=us boot=live nomce module=%(grml_name)s
235 # TODO: extend configuration :)
239 def install_grub(device, dry_run=False):
240 """Install grub on specified device."""
241 logging.critical("TODO: grub-install %s" % device)
244 def install_bootloader(partition, dry_run=False):
245 """Install bootloader on device."""
246 # Install bootloader on the device (/dev/sda),
247 # not on the partition itself (/dev/sda1)
248 if partition[-1:].isdigit():
249 device = re.match(r'(.*?)\d*$', partition).group(1)
254 install_grub(device, dry_run)
256 install_syslinux(device, dry_run)
259 def is_writeable(device):
260 """Check if the device is writeable for the current user"""
264 #raise Exception, "no device for checking write permissions"
266 if not os.path.exists(device):
269 return os.access(device, os.W_OK) and os.access(device, os.R_OK)
271 def install_mbr(device, dry_run=False):
272 """Install a default master boot record on given device
274 @device: device where MBR should be installed to"""
276 if not is_writeable(device):
277 raise IOError, "device not writeable for user"
279 lilo = './lilo/lilo.static' # FIXME
282 raise Exception, "lilo executable not available."
284 # to support -A for extended partitions:
285 logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
286 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
288 if proc.returncode != 0:
289 raise Exception, "error executing lilo"
291 # activate partition:
292 logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
294 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
296 if proc.returncode != 0:
297 raise Exception, "error executing lilo"
299 # lilo's mbr is broken, use the one from syslinux instead:
300 logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
303 # TODO use Popen instead?
304 retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
306 logging.critical("Error copying MBR to device (%s)" % retcode)
307 except OSError, error:
308 logging.critical("Execution failed:", error)
311 def mount(source, target, options):
312 """Mount specified source on given target
314 @source: name of device/ISO that should be mounted
315 @target: directory where the ISO should be mounted to
316 @options: mount specific options"""
318 # notice: dry_run does not work here, as we have to locate files, identify flavour,...
319 logging.debug("mount %s %s %s" % (options, source, target))
320 proc = subprocess.Popen(["mount"] + list(options) + [source, target])
322 if proc.returncode != 0:
323 raise Exception, "Error executing mount"
325 def unmount(directory):
326 """Unmount specified directory
328 @directory: directory where something is mounted on and which should be unmounted"""
330 logging.debug("umount %s" % directory)
331 proc = subprocess.Popen(["umount"] + [directory])
333 if proc.returncode != 0:
334 raise Exception, "Error executing umount"
337 def check_for_vat(partition):
338 """Check whether specified partition is a valid VFAT/FAT16 filesystem
340 @partition: device name of partition"""
343 udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t",
344 partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
345 filesystem = udev_info.communicate()[0].rstrip()
347 if udev_info.returncode == 2:
348 logging.critical("failed to read device %s - wrong UID / permissions?" % partition)
351 if filesystem != "vfat":
355 # * check for ID_FS_VERSION=FAT16 as well?
358 logging.critical("Sorry, /lib/udev/vol_id not available.")
362 def mkdir(directory):
363 """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
365 if not os.path.isdir(directory):
367 os.makedirs(directory)
369 # just silently pass as it's just fine it the directory exists
373 def copy_grml_files(grml_flavour, iso_mount, target, dry_run=False):
374 """Copy files from ISO on given target"""
377 # * provide alternative search_file() if file information is stored in a config.ini file?
378 # * catch "install: .. No space left on device" & CO
379 # * abstract copy logic to make the code shorter and get rid of spaghetti ;)
381 logging.info("Copying files. This might take a while....")
383 squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
384 squashfs_target = target + '/live/'
385 execute(mkdir, squashfs_target)
387 # use install(1) for now to make sure we can write the files afterwards as normal user as well
388 logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
389 proc = execute(subprocess.Popen, ["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
392 filesystem_module = search_file('filesystem.module', iso_mount)
393 logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
394 proc = execute(subprocess.Popen, ["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
397 release_target = target + '/boot/release/' + grml_flavour
398 execute(mkdir, release_target)
400 kernel = search_file('linux26', iso_mount)
401 logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
402 proc = execute(subprocess.Popen, ["install", "--mode=664", kernel, release_target + '/linux26'])
405 initrd = search_file('initrd.gz', iso_mount)
406 logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
407 proc = execute(subprocess.Popen, ["install", "--mode=664", initrd, release_target + '/initrd.gz'])
410 if not options.copyonly:
411 isolinux_target = target + '/boot/isolinux/'
412 execute(mkdir, isolinux_target)
414 # FIXME - Fatal: could not identify grml flavour, sorry.
415 logo = search_file('logo.16', iso_mount)
416 logging.debug("cp %s %s" % logo, isolinux_target + 'logo.16')
417 proc = execute(subprocess.Popen, ["install", "--mode=664", logo, isolinux_target + 'logo.16'])
420 for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
421 bootsplash = search_file(ffile, iso_mount)
422 logging.debug("cp %s %s" % (bootsplash, isolinux_target + ffile))
423 proc = execute(subprocess.Popen, ["install", "--mode=664", bootsplash, isolinux_target + ffile])
426 grub_target = target + '/boot/grub/'
427 execute(mkdir, grub_target)
429 logging.debug("cp grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz')
430 proc = execute(subprocess.Popen, ["install", "--mode=664", 'grub/splash.xpm.gz', grub_target + 'splash.xpm.gz'])
433 logging.debug("cp grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito')
434 proc = execute(subprocess.Popen, ["install", "--mode=664", 'grub/stage2_eltorito', grub_target + 'stage2_eltorito'])
437 logging.debug("Generating grub configuration %s" % grub_target + 'menu.lst')
439 #with open("...", "w") as f:
440 #f.write("bla bla bal")
441 grub_config_file = open(grub_target + 'menu.lst', 'w')
442 grub_config_file.write(generate_grub_config(grml_flavour))
443 grub_config_file.close( )
445 syslinux_target = target + '/boot/isolinux/'
446 execute(mkdir, syslinux_target)
448 logging.debug("Generating syslinux configuration %s" % syslinux_target + 'syslinux.cfg')
450 syslinux_config_file = open(syslinux_target + 'syslinux.cfg', 'w')
451 syslinux_config_file.write(generate_syslinux_config(grml_flavour))
452 syslinux_config_file.close( )
454 logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
456 isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
457 isolinux_splash.write(generate_isolinux_splash(grml_flavour))
458 isolinux_splash.close( )
461 # make sure we are sync before continuing
462 proc = subprocess.Popen(["sync"])
465 def uninstall_files(device):
466 """Get rid of all grml files on specified device"""
469 logging.critical("TODO: %s" % device)
472 def identify_grml_flavour(mountpath):
473 """Get name of grml flavour
475 @mountpath: path where the grml ISO is mounted to
476 @return: name of grml-flavour"""
478 version_file = search_file('grml-version', mountpath)
480 if version_file == "":
481 logging.critical("Error: could not find grml-version file.")
485 tmpfile = open(version_file, 'r')
486 grml_info = tmpfile.readline()
487 grml_flavour = re.match(r'[\w-]*', grml_info).group()
491 logging.critical("Unexpected error:", sys.exc_info()[0])
496 def handle_iso(iso, device):
500 logging.info("iso = %s" % iso)
502 if os.path.isdir(iso):
503 logging.critical("TODO: /live/image handling not yet implemented") # TODO
505 iso_mountpoint = tempfile.mkdtemp()
506 remove_iso_mountpoint = True
507 mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
509 if os.path.isdir(device):
510 logging.debug("Specified target is a directory, not mounting therefore.")
511 device_mountpoint = device
512 remove_device_mountpoint = False
516 device_mountpoint = tempfile.mkdtemp()
517 remove_device_mountpoint = True
518 mount(device, device_mountpoint, "")
521 grml_flavour = identify_grml_flavour(iso_mountpoint)
522 logging.info("Identified grml flavour \"%s\"." % grml_flavour)
523 copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint, dry_run=options.dryrun)
525 logging.critical("Fatal: could not identify grml flavour, sorry.")
528 if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
529 unmount(iso_mountpoint)
530 os.rmdir(iso_mountpoint)
531 if os.path.isdir(device_mountpoint) and remove_device_mountpoint:
532 unmount(device_mountpoint)
533 os.rmdir(device_mountpoint)
535 # grml_flavour_short = grml_flavour.replace('-','')
536 # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
540 """Main function [make pylint happy :)]"""
543 print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
547 parser.error("invalid usage")
550 FORMAT = "%(asctime)-15s %(message)s"
551 logging.basicConfig(level=logging.DEBUG, format=FORMAT)
553 FORMAT = "Critial: %(message)s"
554 logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
556 FORMAT = "Info: %(message)s"
557 logging.basicConfig(level=logging.INFO, format=FORMAT)
560 logging.info("Running in simulate mode as requested via option dry-run.")
564 device = args[len(args) - 1]
565 isos = args[0:len(args) - 1]
567 if not which("syslinux"):
568 logging.critical('Sorry, syslinux not available. Exiting.')
569 logging.critical('Please install syslinux or consider using the --grub option.')
573 # * check for valid blockdevice, vfat and mount functions
574 # if device is not None:
575 # check_for_vat(device)
576 # mount_target(partition)
579 handle_iso(iso, device)
581 if options.mbr and not skip_mbr:
582 # make sure we install MBR on /dev/sdX and not /dev/sdX#
583 if device[-1:].isdigit():
584 device = re.match(r'(.*?)\d*$', device).group(1)
587 install_mbr(device, dry_run=options.dryrun)
588 except IOError, error:
589 logging.critical("Execution failed:", error)
591 except Exception, error:
592 logging.critical("Execution failed:", error)
596 logging.info("Not installing bootloader and its files as requested via option copyonly.")
598 install_bootloader(device, dry_run=options.dryrun)
600 logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
602 if __name__ == "__main__":
605 except KeyboardInterrupt:
606 print "TODO: handle me! :)"
608 ## END OF FILE #################################################################
609 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8