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 * implement missing options (--kernel, --initrd, --uninstall,...)
17 * improve error handling :)
18 * get rid of "if not dry_run" inside code/functions
19 * implement mount handling
20 * implement logic for storing information about copied files
21 -> register every single file?
22 * trap handling (like unmount devices when interrupting?)
23 * get rid of all TODOs in code :)
24 * graphical version? :)
27 from __future__ import with_statement
28 import os, re, subprocess, sys, tempfile
29 from optparse import OptionParser
30 from os.path import exists, join, abspath
31 from os import pathsep
32 from inspect import isroutine, isclass
35 PROG_VERSION = "0.0.1"
38 usage = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/ice>\n\
40 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
41 Make sure you have at least a grml ISO or a running grml system (/live/image),\n\
42 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
45 parser = OptionParser(usage=usage)
46 parser.add_option("--bootoptions", dest="bootoptions",
47 action="store", type="string",
48 help="use specified bootoptions as defaut")
49 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
50 help="do not copy files only but just install a bootloader")
51 parser.add_option("--copy-only", dest="copyonly", action="store_true",
52 help="copy files only and do not install bootloader")
53 parser.add_option("--dry-run", dest="dryrun", action="store_true",
54 help="do not actually execute any commands")
55 parser.add_option("--fat16", dest="fat16", action="store_true",
56 help="format specified partition with FAT16")
57 parser.add_option("--force", dest="force", action="store_true",
58 help="force any actions requiring manual interaction")
59 parser.add_option("--grub", dest="grub", action="store_true",
60 help="install grub bootloader instead of syslinux")
61 parser.add_option("--initrd", dest="initrd", action="store", type="string",
62 help="install specified initrd instead of the default")
63 parser.add_option("--kernel", dest="kernel", action="store", type="string",
64 help="install specified kernel instead of the default")
65 parser.add_option("--mbr", dest="mbr", action="store_true",
66 help="install master boot record (MBR) on the device")
67 parser.add_option("--quiet", dest="quiet", action="store_true",
68 help="do not output anything than errors on console")
69 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
70 help="install specified squashfs file instead of the default")
71 parser.add_option("--uninstall", dest="uninstall", action="store_true",
72 help="remove grml ISO files")
73 parser.add_option("--verbose", dest="verbose", action="store_true",
74 help="enable verbose mode")
75 parser.add_option("-v", "--version", dest="version", action="store_true",
76 help="display version and exit")
77 (options, args) = parser.parse_args()
80 def get_function_name(obj):
81 if not (isroutine(obj) or isclass(obj)):
83 return obj.__module__ + '.' + obj.__name__
85 def execute(f, *args):
86 """Wrapper for executing a command. Either really executes
87 the command (default) or when using --dry-run commandline option
88 just displays what would be executed."""
89 # demo: execute(subprocess.Popen, (["ls", "-la"]))
91 logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, args))))
97 """Check whether a given file can be executed
99 @fpath: full path to file
101 return os.path.exists(fpath) and os.access(fpath, os.X_OK)
105 """Check whether a given program is available in PATH
107 @program: name of executable"""
108 fpath, fname = os.path.split(program)
113 for path in os.environ["PATH"].split(os.pathsep):
114 exe_file = os.path.join(path, program)
121 def search_file(filename, search_path='/bin' + pathsep + '/usr/bin'):
122 """Given a search path, find file"""
124 paths = search_path.split(pathsep)
126 for current_dir, directories, files in os.walk(path):
127 if exists(join(current_dir, filename)):
131 return abspath(join(current_dir, filename))
136 def check_uid_root():
137 """Check for root permissions"""
138 if not os.geteuid()==0:
139 sys.exit("Error: please run this script with uid 0 (root).")
142 def install_syslinux(device, dry_run=False):
144 """Install syslinux on specified device."""
145 logging.critical("debug: syslinux %s [TODO]" % device)
147 # syslinux -d boot/isolinux /dev/usb-sdb1
150 def generate_grub_config(grml_flavour):
151 """Generate grub configuration for use via menu,lst"""
154 # * install main part of configuration just *once* and append
155 # flavour specific configuration only
156 # * what about systems using grub2 without having grub1 available?
159 grml_name = grml_flavour
164 # color red/blue green/black
165 splashimage=/boot/grub/splash.xpm.gz
170 title %(grml_name)s - Default boot (using 1024x768 framebuffer)
171 kernel /boot/release/%(grml_name)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_name)s
172 initrd /boot/release/%(grml_name)s/initrd.gz
174 # TODO: extend configuration :)
178 def generate_isolinux_splash(grml_flavour):
179 """Generate bootsplash for isolinux/syslinux"""
182 # * adjust last bootsplash line
184 grml_name = grml_flavour
187 \ f17
\f\18/boot/isolinux/logo.16
189 Some information and boot options available via keys F2 - F10. http://grml.org/
193 def generate_syslinux_config(grml_flavour):
194 """Generate configuration for use in syslinux.cfg"""
197 # * install main part of configuration just *once* and append
198 # flavour specific configuration only
199 # * unify isolinux and syslinux setup ("INCLUDE /boot/...")
202 grml_name = grml_flavour
205 # use this to control the bootup via a serial port
210 DISPLAY /boot/isolinux/boot.msg
211 F1 /boot/isolinux/boot.msg
220 F10 /boot/isolinux/f10
223 KERNEL /boot/release/%(grml_name)s/linux26
224 APPEND initrd=/boot/release/%(grml_name)s/initrd.gz apm=power-off lang=us boot=live nomce module=%(grml_name)s
226 # TODO: extend configuration :)
230 def install_grub(device, dry_run=False):
231 """Install grub on specified device."""
232 logging.critical("TODO: grub-install %s" % device)
235 def install_bootloader(partition, dry_run=False):
236 """Install bootloader on device."""
237 # Install bootloader on the device (/dev/sda),
238 # not on the partition itself (/dev/sda1)
239 if partition[-1:].isdigit():
240 device = re.match(r'(.*?)\d*$', partition).group(1)
245 install_grub(device, dry_run)
247 install_syslinux(device, dry_run)
250 def is_writeable(device):
251 """Check if the device is writeable for the current user"""
255 #raise Exception, "no device for checking write permissions"
257 if not os.path.exists(device):
260 return os.access(device, os.W_OK) and os.access(device, os.R_OK)
262 def install_mbr(device, dry_run=False):
263 """Install a default master boot record on given device
265 @device: device where MBR should be installed to"""
267 if not is_writeable(device):
268 raise IOError, "device not writeable for user"
270 lilo = './lilo/lilo.static' # FIXME
273 raise Exception, "lilo executable not available."
275 # to support -A for extended partitions:
276 logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
277 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
279 if proc.returncode != 0:
280 raise Exception, "error executing lilo"
282 # activate partition:
283 logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
285 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
287 if proc.returncode != 0:
288 raise Exception, "error executing lilo"
290 # lilo's mbr is broken, use the one from syslinux instead:
291 logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
294 # TODO use Popen instead?
295 retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
297 logging.critical("Error copying MBR to device (%s)" % retcode)
298 except OSError, error:
299 logging.critical("Execution failed:", error)
302 def mount(source, target, options, dry_run=False):
303 """Mount specified source on given target
305 @source: name of device/ISO that should be mounted
306 @target: directory where the ISO should be mounted to
307 @options: mount specific options"""
310 logging.critial("TODO: mount %s %s %s" % (options, source, target))
312 logging.debug("TODO: mount %s %s %s" % (options, source, target))
315 def unmount(directory, dry_run=False):
316 """Unmount specified directory
318 @directory: directory where something is mounted on and which should be unmounted"""
321 logging.info("TODO: umount %s" % directory)
323 logging.debug("TODO: umount %s" % directory)
326 def check_for_vat(partition):
327 """Check whether specified partition is a valid VFAT/FAT16 filesystem
329 @partition: device name of partition"""
332 udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t",
333 partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
334 filesystem = udev_info.communicate()[0].rstrip()
336 if udev_info.returncode == 2:
337 logging.critical("failed to read device %s - wrong UID / permissions?" % partition)
340 if filesystem != "vfat":
344 # * check for ID_FS_VERSION=FAT16 as well?
347 logging.critical("Sorry, /lib/udev/vol_id not available.")
351 def mkdir(directory):
352 """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
354 if not os.path.isdir(directory):
356 os.makedirs(directory)
358 # just silently pass as it's just fine it the directory exists
362 def copy_grml_files(grml_flavour, iso_mount, target, dry_run=False):
363 """Copy files from ISO on given target"""
366 # * provide alternative search_file() if file information is stored in a config.ini file?
367 # * catch "install: .. No space left on device" & CO
368 # * abstract copy logic to make the code shorter and get rid of spaghetti ;)
370 logging.info("Copying files. This might take a while....")
372 squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
373 squashfs_target = target + '/live/'
374 execute(mkdir, squashfs_target)
376 # use install(1) for now to make sure we can write the files afterwards as normal user as well
377 logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
378 execute(subprocess.Popen, ["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
380 filesystem_module = search_file('filesystem.module', iso_mount)
381 logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
382 execute(subprocess.Popen, ["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
384 release_target = target + '/boot/release/' + grml_flavour
385 execute(mkdir, release_target)
387 kernel = search_file('linux26', iso_mount)
388 logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
389 execute(subprocess.Popen, ["install", "--mode=664", kernel, release_target + '/linux26'])
391 initrd = search_file('initrd.gz', iso_mount)
392 logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
393 execute(subprocess.Popen, ["install", "--mode=664", initrd, release_target + '/initrd.gz'])
395 if not options.copyonly:
396 isolinux_target = target + '/boot/isolinux/'
397 execute(mkdir, isolinux_target)
399 logo = search_file('logo.16', iso_mount)
400 logging.debug("cp %s %s" % logo, isolinux_target + 'logo.16')
401 execute(subprocess.Popen, ["install", "--mode=664", logo, isolinux_target + 'logo.16'])
403 for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
404 bootsplash = search_file(ffile, iso_mount)
405 logging.debug("cp %s %s" % (bootsplash, isolinux_target + ffile))
406 execute(subprocess.Popen, ["install", "--mode=664", bootsplash, isolinux_target + ffile])
408 grub_target = target + '/boot/grub/'
409 execute(mkdir, grub_target)
411 logging.debug("cp grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz')
412 execute(subprocess.Popen, ["install", "--mode=664", 'grub/splash.xpm.gz', grub_target + 'splash.xpm.gz'])
414 logging.debug("cp grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito')
415 execute(subprocess.Popen, ["install", "--mode=664", 'grub/stage2_eltorito', grub_target + 'stage2_eltorito'])
417 logging.debug("Generating grub configuration %s" % grub_target + 'menu.lst')
419 #with open("...", "w") as f:
420 #f.write("bla bla bal")
421 grub_config_file = open(grub_target + 'menu.lst', 'w')
422 grub_config_file.write(generate_grub_config(grml_flavour))
423 grub_config_file.close( )
425 syslinux_target = target + '/boot/isolinux/'
426 execute(mkdir, syslinux_target)
428 logging.debug("Generating syslinux configuration %s" % syslinux_target + 'syslinux.cfg')
430 syslinux_config_file = open(syslinux_target + 'syslinux.cfg', 'w')
431 syslinux_config_file.write(generate_syslinux_config(grml_flavour))
432 syslinux_config_file.close( )
434 logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
436 isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
437 isolinux_splash.write(generate_isolinux_splash(grml_flavour))
438 isolinux_splash.close( )
441 def uninstall_files(device):
442 """Get rid of all grml files on specified device"""
445 logging.critical("TODO: %s" % device)
448 def identify_grml_flavour(mountpath):
449 """Get name of grml flavour
451 @mountpath: path where the grml ISO is mounted to
452 @return: name of grml-flavour"""
454 version_file = search_file('grml-version', mountpath)
456 if version_file == "":
457 logging.critical("Error: could not find grml-version file.")
461 tmpfile = open(version_file, 'r')
462 grml_info = tmpfile.readline()
463 grml_flavour = re.match(r'[\w-]*', grml_info).group()
467 logging.critical("Unexpected error:", sys.exc_info()[0])
472 def handle_iso(iso, device):
476 logging.info("iso = %s" % iso)
478 if os.path.isdir(iso):
479 logging.critical("TODO: /live/image handling not yet implemented") # TODO
481 iso_mountpoint = '/mnt/test' # FIXME
482 # iso_mount = tempfile.mkdtemp()
483 mount(iso, iso_mountpoint, "-o loop -t iso9660", dry_run=options.dryrun)
484 # device_mountpoint = '/mnt/usb-sdb1'
485 # device_mountpoint = tempfile.mkdtemp()
486 device_mountpoint = '/dev/shm/grml2usb' # FIXME
487 mount(device, device_mountpoint, "", dry_run=options.dryrun)
490 grml_flavour = identify_grml_flavour(iso_mountpoint)
491 logging.info("Identified grml flavour \"%s\"." % grml_flavour)
493 logging.critical("Fatal: could not identify grml flavour, sorry.")
496 # grml_flavour_short = grml_flavour.replace('-','')
497 # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
499 copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint, dry_run=options.dryrun)
501 unmount(device_mountpoint, dry_run=options.dryrun) # TODO
502 unmount(iso_mountpoint, dry_run=options.dryrun) # TODO
504 #if os.path.isdir(target):
506 #if os.path.isdir(iso_mount):
507 # os.rmdir(iso_mount)
510 """Main function [make pylint happy :)]"""
513 print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
517 parser.error("invalid usage")
520 FORMAT = "%(asctime)-15s %(message)s"
521 logging.basicConfig(level=logging.DEBUG, format=FORMAT)
523 FORMAT = "Critial: %(message)s"
524 logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
526 FORMAT = "Info: %(message)s"
527 logging.basicConfig(level=logging.INFO, format=FORMAT)
530 logging.info("Running in simulate mode as requested via option dry-run.")
534 device = args[len(args) - 1]
535 isos = args[0:len(args) - 1]
537 if not which("syslinux"):
538 logging.critical('Sorry, syslinux not available. Exiting.')
539 logging.critical('Please install syslinux or consider using the --grub option.')
543 # * check for valid blockdevice, vfat and mount functions
544 # if device is not None:
545 # check_for_vat(device)
546 # mount_target(partition)
549 # * it doesn't need to be a ISO, could be /live/image as well
551 handle_iso(iso, device)
554 # make sure we install MBR on /dev/sdX and not /dev/sdX#
555 if device[-1:].isdigit():
556 device = re.match(r'(.*?)\d*$', device).group(1)
559 install_mbr(device, dry_run=options.dryrun)
560 except IOError, error:
561 logging.critical("Execution failed:", error)
563 except Exception, error:
564 logging.critical("Execution failed:", error)
568 logging.info("Not installing bootloader and its files as requested via option copyonly.")
570 install_bootloader(device, dry_run=options.dryrun)
572 logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
574 if __name__ == "__main__":
577 ## END OF FILE #################################################################
578 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8