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 * verify that the specified device is really an USB device (/sys/devices/*/removable_)
17 * validate partition schema/layout:
19 -> fat16 partition if using syslinux
20 * implement missing options (--kernel, --initrd, --uninstall,...)
21 * improve error handling :)
22 * implement logic for storing information about copied files
23 -> register every single file?
24 * get rid of all TODOs in code :)
25 * graphical version? :)
28 from __future__ import with_statement
29 import os, re, subprocess, sys, tempfile
30 from optparse import OptionParser
31 from os.path import exists, join, abspath
32 from os import pathsep
33 from inspect import isroutine, isclass
36 PROG_VERSION = "0.0.1"
38 skip_mbr = False # By default we don't want to skip it; TODO - can we get rid of that?
41 usage = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/ice>\n\
43 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
44 Make sure you have at least a grml ISO or a running grml system (/live/image),\n\
45 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
48 parser = OptionParser(usage=usage)
49 parser.add_option("--bootoptions", dest="bootoptions",
50 action="store", type="string",
51 help="use specified bootoptions as defaut")
52 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
53 help="do not copy files only but just install a bootloader")
54 parser.add_option("--copy-only", dest="copyonly", action="store_true",
55 help="copy files only and do not install bootloader")
56 parser.add_option("--dry-run", dest="dryrun", action="store_true",
57 help="do not actually execute any commands")
58 parser.add_option("--fat16", dest="fat16", action="store_true",
59 help="format specified partition with FAT16")
60 parser.add_option("--force", dest="force", action="store_true",
61 help="force any actions requiring manual interaction")
62 parser.add_option("--grub", dest="grub", action="store_true",
63 help="install grub bootloader instead of syslinux")
64 parser.add_option("--initrd", dest="initrd", action="store", type="string",
65 help="install specified initrd instead of the default")
66 parser.add_option("--kernel", dest="kernel", action="store", type="string",
67 help="install specified kernel instead of the default")
68 parser.add_option("--mbr", dest="mbr", action="store_true",
69 help="install master boot record (MBR) on the device")
70 parser.add_option("--mountpath", dest="mountpath", action="store_true",
71 help="install system to specified mount path")
72 parser.add_option("--quiet", dest="quiet", action="store_true",
73 help="do not output anything than errors on console")
74 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
75 help="install specified squashfs file instead of the default")
76 parser.add_option("--uninstall", dest="uninstall", action="store_true",
77 help="remove grml ISO files")
78 parser.add_option("--verbose", dest="verbose", action="store_true",
79 help="enable verbose mode")
80 parser.add_option("-v", "--version", dest="version", action="store_true",
81 help="display version and exit")
82 (options, args) = parser.parse_args()
85 def get_function_name(obj):
86 if not (isroutine(obj) or isclass(obj)):
88 return obj.__module__ + '.' + obj.__name__
91 def execute(f, *args):
92 """Wrapper for executing a command. Either really executes
93 the command (default) or when using --dry-run commandline option
94 just displays what would be executed."""
95 # demo: execute(subprocess.Popen, (["ls", "-la"]))
97 logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, args))))
103 """Check whether a given file can be executed
105 @fpath: full path to file
107 return os.path.exists(fpath) and os.access(fpath, os.X_OK)
111 """Check whether a given program is available in PATH
113 @program: name of executable"""
114 fpath, fname = os.path.split(program)
119 for path in os.environ["PATH"].split(os.pathsep):
120 exe_file = os.path.join(path, program)
127 def search_file(filename, search_path='/bin' + pathsep + '/usr/bin'):
128 """Given a search path, find file"""
130 paths = search_path.split(pathsep)
132 for current_dir, directories, files in os.walk(path):
133 if exists(join(current_dir, filename)):
137 return abspath(join(current_dir, filename))
142 def check_uid_root():
143 """Check for root permissions"""
144 if not os.geteuid()==0:
145 sys.exit("Error: please run this script with uid 0 (root).")
148 def install_syslinux(device, dry_run=False):
150 """Install syslinux on specified device."""
152 # syslinux -d boot/isolinux /dev/sdb1
153 logging.info("Installing syslinux")
154 logging.debug("syslinux -d boot/syslinux %s" % device)
155 proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
157 if proc.returncode != 0:
158 raise Exception, "error executing syslinux"
161 def generate_grub_config(grml_flavour):
162 """Generate grub configuration for use via menu,lst"""
165 # * install main part of configuration just *once* and append
166 # flavour specific configuration only
167 # * what about systems using grub2 without having grub1 available?
173 # color red/blue green/black
174 splashimage=/boot/grub/splash.xpm.gz
179 title %(grml_flavour)s - Default boot (using 1024x768 framebuffer)
180 kernel /boot/release/%(grml_flavour)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_flavour)s
181 initrd /boot/release/%(grml_flavour)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/syslinux/logo.16
198 Some information and boot options available via keys F2 - F10. http://grml.org/
202 def generate_main_syslinux_config(grml_flavour, grml_bootoptions):
203 """Generate main 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/...")
212 ## main syslinux configuration - generated by grml2usb
213 # use this to control the bootup via a serial port
218 DISPLAY /boot/syslinux/boot.msg
219 F1 /boot/syslinux/boot.msg
228 F10 /boot/syslinux/f10
229 ## end of main configuration
231 # flavour specific configuration for grml
233 KERNEL /boot/release/%(grml_flavour)s/linux26
234 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(grml_bootoptions)s
238 def generate_flavour_specific_syslinux_config(grml_flavour, bootoptions):
239 """Generate flavour specific configuration for use in syslinux.cfg"""
243 # flavour specific configuration for %(grml_flavour)s
244 LABEL %(grml_flavour)s
245 KERNEL /boot/release/%(grml_flavour)s/linux26
246 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(bootoptions)s
250 def install_grub(device, dry_run=False):
251 """Install grub on specified device."""
252 logging.critical("TODO: grub-install %s" % device)
255 def install_bootloader(partition, dry_run=False):
256 """Install bootloader on device."""
258 # Install bootloader on the device (/dev/sda),
259 # not on the partition itself (/dev/sda1)?
260 # if partition[-1:].isdigit():
261 # device = re.match(r'(.*?)\d*$', partition).group(1)
266 install_grub(partition, dry_run)
268 install_syslinux(partition, dry_run)
271 def is_writeable(device):
272 """Check if the device is writeable for the current user"""
276 #raise Exception, "no device for checking write permissions"
278 if not os.path.exists(device):
281 return os.access(device, os.W_OK) and os.access(device, os.R_OK)
283 def install_mbr(device, dry_run=False):
284 """Install a default master boot record on given device
286 @device: device where MBR should be installed to"""
288 if not is_writeable(device):
289 raise IOError, "device not writeable for user"
291 lilo = '/grml/git/grml2usb/lilo/lilo.static' # FIXME
294 raise Exception, "lilo executable not available."
296 # to support -A for extended partitions:
297 logging.info("Installing MBR")
298 logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
299 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
301 if proc.returncode != 0:
302 raise Exception, "error executing lilo"
304 # activate partition:
305 logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
307 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
309 if proc.returncode != 0:
310 raise Exception, "error executing lilo"
312 # lilo's mbr is broken, use the one from syslinux instead:
313 logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
316 # TODO use Popen instead?
317 retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
319 logging.critical("Error copying MBR to device (%s)" % retcode)
320 except OSError, error:
321 logging.critical("Execution failed:", error)
324 def mount(source, target, options):
325 """Mount specified source on given target
327 @source: name of device/ISO that should be mounted
328 @target: directory where the ISO should be mounted to
329 @options: mount specific options"""
331 # notice: dry_run does not work here, as we have to locate files, identify flavour,...
332 logging.debug("mount %s %s %s" % (options, source, target))
333 proc = subprocess.Popen(["mount"] + list(options) + [source, target])
335 if proc.returncode != 0:
336 raise Exception, "Error executing mount"
338 def unmount(directory):
339 """Unmount specified directory
341 @directory: directory where something is mounted on and which should be unmounted"""
343 logging.debug("umount %s" % directory)
344 proc = subprocess.Popen(["umount"] + [directory])
346 if proc.returncode != 0:
347 raise Exception, "Error executing umount"
350 def check_for_vat(partition):
351 """Check whether specified partition is a valid VFAT/FAT16 filesystem
353 @partition: device name of partition"""
356 udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t",
357 partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
358 filesystem = udev_info.communicate()[0].rstrip()
360 if udev_info.returncode == 2:
361 logging.critical("failed to read device %s - wrong UID / permissions?" % partition)
364 if filesystem != "vfat":
368 # * check for ID_FS_VERSION=FAT16 as well?
371 logging.critical("Sorry, /lib/udev/vol_id not available.")
375 def mkdir(directory):
376 """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
378 if not os.path.isdir(directory):
380 os.makedirs(directory)
382 # just silently pass as it's just fine it the directory exists
386 def copy_grml_files(grml_flavour, iso_mount, target, dry_run=False):
387 """Copy files from ISO on given target"""
390 # * provide alternative search_file() if file information is stored in a config.ini file?
391 # * catch "install: .. No space left on device" & CO
392 # * abstract copy logic to make the code shorter and get rid of spaghetti ;)
394 logging.info("Copying files. This might take a while....")
396 squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
397 squashfs_target = target + '/live/'
398 execute(mkdir, squashfs_target)
400 # use install(1) for now to make sure we can write the files afterwards as normal user as well
401 logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
402 proc = execute(subprocess.Popen, ["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
405 filesystem_module = search_file('filesystem.module', iso_mount)
406 logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
407 proc = execute(subprocess.Popen, ["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
410 release_target = target + '/boot/release/' + grml_flavour
411 execute(mkdir, release_target)
413 kernel = search_file('linux26', iso_mount)
414 logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
415 proc = execute(subprocess.Popen, ["install", "--mode=664", kernel, release_target + '/linux26'])
418 initrd = search_file('initrd.gz', iso_mount)
419 logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
420 proc = execute(subprocess.Popen, ["install", "--mode=664", initrd, release_target + '/initrd.gz'])
423 if not options.copyonly:
424 syslinux_target = target + '/boot/syslinux/'
425 execute(mkdir, syslinux_target)
427 logo = search_file('logo.16', iso_mount)
428 logging.debug("cp %s %s" % (logo, syslinux_target + 'logo.16'))
429 proc = execute(subprocess.Popen, ["install", "--mode=664", logo, syslinux_target + 'logo.16'])
432 for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
433 bootsplash = search_file(ffile, iso_mount)
434 logging.debug("cp %s %s" % (bootsplash, syslinux_target + ffile))
435 proc = execute(subprocess.Popen, ["install", "--mode=664", bootsplash, syslinux_target + ffile])
438 grub_target = target + '/boot/grub/'
439 execute(mkdir, grub_target)
441 logging.debug("cp /grml/git/grml2usb/grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz') # FIXME - path of grub
442 proc = execute(subprocess.Popen, ["install", "--mode=664", '/grml/git/grml2usb/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz']) # FIXME
445 logging.debug("cp /grml/git/grml2usb/grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito') # FIXME - path of grub
446 proc = execute(subprocess.Popen, ["install", "--mode=664", '/grml/git/grml2usb/grub/stage2_eltorito', grub_target + 'stage2_eltorito']) # FIXME
450 logging.debug("Generating grub configuration") # % grub_target + 'menu.lst')
451 #with open("...", "w") as f:
452 #f.write("bla bla bal")
453 grub_config_file = open(grub_target + 'menu.lst', 'w')
454 grub_config_file.write(generate_grub_config(grml_flavour))
455 grub_config_file.close()
457 logging.info("Generating syslinux configuration") # % syslinux_target + 'syslinux.cfg')
458 syslinux_cfg = syslinux_target + 'syslinux.cfg'
460 # install main configuration only *once*, no matter how many ISOs we have:
461 if os.path.isfile(syslinux_cfg):
462 string = open(syslinux_cfg).readline()
463 if not re.match("## main syslinux configuration", string):
464 syslinux_config_file = open(syslinux_cfg, 'w')
465 syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, "")) # FIXME - bootoptions
466 syslinux_config_file.close()
468 syslinux_config_file = open(syslinux_cfg, 'w')
469 syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, "")) # FIXME - bootoptions
470 syslinux_config_file.close()
472 # install flavour specific configuration only *once* as well
473 # ugly - I'm pretty sure this could be smoother...
474 flavour_config = True
475 if os.path.isfile(syslinux_cfg):
476 string = open(syslinux_cfg).readlines()
477 flavour = re.compile("^# flavour specific configuration for %s" % re.escape(grml_flavour))
479 if flavour.match(line):
480 flavour_config = False
483 syslinux_config_file = open(syslinux_cfg, 'a')
484 syslinux_config_file.write(generate_flavour_specific_syslinux_config(grml_flavour, "")) # FIXME - bootoptions
485 syslinux_config_file.close( )
487 logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
488 isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
489 isolinux_splash.write(generate_isolinux_splash(grml_flavour))
490 isolinux_splash.close( )
493 # make sure we are sync before continuing
494 proc = subprocess.Popen(["sync"])
497 def uninstall_files(device):
498 """Get rid of all grml files on specified device"""
501 logging.critical("TODO: %s" % device)
504 def identify_grml_flavour(mountpath):
505 """Get name of grml flavour
507 @mountpath: path where the grml ISO is mounted to
508 @return: name of grml-flavour"""
510 version_file = search_file('grml-version', mountpath)
512 if version_file == "":
513 logging.critical("Error: could not find grml-version file.")
517 tmpfile = open(version_file, 'r')
518 grml_info = tmpfile.readline()
519 grml_flavour = re.match(r'[\w-]*', grml_info).group()
523 logging.critical("Unexpected error:", sys.exc_info()[0])
528 def handle_iso(iso, device):
532 logging.info("Using ISO %s" % iso)
534 if os.path.isdir(iso):
535 logging.critical("TODO: /live/image handling not yet implemented") # TODO
537 iso_mountpoint = tempfile.mkdtemp()
538 remove_iso_mountpoint = True
539 mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
541 if os.path.isdir(device):
542 logging.info("Specified target is a directory, not mounting therefore.")
543 device_mountpoint = device
544 remove_device_mountpoint = False
548 device_mountpoint = tempfile.mkdtemp()
549 remove_device_mountpoint = True
550 mount(device, device_mountpoint, "")
553 grml_flavour = identify_grml_flavour(iso_mountpoint)
554 logging.info("Identified grml flavour \"%s\"." % grml_flavour)
555 copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint, dry_run=options.dryrun)
557 logging.critical("Fatal: something happend - TODO")
560 if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
561 unmount(iso_mountpoint)
562 os.rmdir(iso_mountpoint)
564 if remove_device_mountpoint:
565 unmount(device_mountpoint)
567 if os.path.isdir(device_mountpoint):
568 os.rmdir(device_mountpoint)
570 # grml_flavour_short = grml_flavour.replace('-','')
571 # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
575 """Main function [make pylint happy :)]"""
578 print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
582 parser.error("invalid usage")
585 FORMAT = "%(asctime)-15s %(message)s"
586 logging.basicConfig(level=logging.DEBUG, format=FORMAT)
588 FORMAT = "Critial: %(message)s"
589 logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
591 FORMAT = "Info: %(message)s"
592 logging.basicConfig(level=logging.INFO, format=FORMAT)
595 logging.info("Running in simulate mode as requested via option dry-run.")
599 device = args[len(args) - 1]
600 isos = args[0:len(args) - 1]
602 # make sure we can replace old grml2usb script and warn user when using old way of life:
603 if device.startswith("/mnt/external") or device.startswith("/mnt/usb"):
604 print "Warning: the semantics of grml2usb has changed."
605 print "Instead of using grml2usb /path/to/iso %s you might" % device
606 print "want to use grml2usb /path/to/iso /dev/... instead."
607 print "Please check out the grml2usb manpage for details."
608 f = raw_input("Do you really want to continue? y/N ")
609 if f == "y" or f == "Y":
614 if not which("syslinux"):
615 logging.critical('Sorry, syslinux not available. Exiting.')
616 logging.critical('Please install syslinux or consider using the --grub option.')
620 # * check for valid blockdevice, vfat and mount functions
621 # if device is not None:
622 # check_for_vat(device)
625 handle_iso(iso, device)
627 if options.mbr and not skip_mbr:
629 # make sure we install MBR on /dev/sdX and not /dev/sdX#
630 if device[-1:].isdigit():
631 mbr_device = re.match(r'(.*?)\d*$', device).group(1)
634 install_mbr(mbr_device, dry_run=options.dryrun)
635 except IOError, error:
636 logging.critical("Execution failed:", error)
638 except Exception, error:
639 logging.critical("Execution failed:", error)
643 logging.info("Not installing bootloader and its files as requested via option copyonly.")
645 install_bootloader(device, dry_run=options.dryrun)
647 logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
649 if __name__ == "__main__":
652 except KeyboardInterrupt:
653 print "TODO / FIXME: handle me! :)"
655 ## END OF FILE #################################################################
656 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8