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/
17 - improve error handling :)
18 - get rid of all TODOs in code :)
19 - use 'with open("...", "w") as f: ... f.write("...")'
20 - simplify functions/code as much as possible -> audit
21 * implement missing options (--kernel, --initrd, --uninstall,...)
22 * validate partition schema/layout: is the partition schema ok and the bootable flag set?
23 * implement logic for storing information about copied files -> register every file in a set()
24 * the last line in bootsplash (boot.msg) should mention all installed grml flavours
25 * extend flavour's syslinux configuration
26 * graphical version? :)
29 from __future__ import with_statement
30 import os, re, subprocess, sys, tempfile
31 from optparse import OptionParser
32 from os.path import exists, join, abspath
33 from os import pathsep
34 from inspect import isroutine, isclass
39 PROG_VERSION = "0.0.1"
40 skip_mbr = False # By default we don't want to skip it; TODO - can we get rid of that?
41 mounted = set() # register mountpoints
42 tmpfiles = set() # register tmpfiles
43 datestamp= time.mktime(datetime.datetime.now().timetuple()) # unique identifier for syslinux.cfg
46 usage = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/ice>\n\
48 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
49 Make sure you have at least a grml ISO or a running grml system (/live/image),\n\
50 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
53 parser = OptionParser(usage=usage)
54 parser.add_option("--bootoptions", dest="bootoptions",
55 action="store", type="string",
56 help="use specified bootoptions as defaut")
57 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
58 help="do not copy files only but just install a bootloader")
59 parser.add_option("--copy-only", dest="copyonly", action="store_true",
60 help="copy files only and do not install bootloader")
61 parser.add_option("--dry-run", dest="dryrun", action="store_true",
62 help="do not actually execute any commands")
63 parser.add_option("--fat16", dest="fat16", action="store_true",
64 help="format specified partition with FAT16")
65 parser.add_option("--force", dest="force", action="store_true",
66 help="force any actions requiring manual interaction")
67 parser.add_option("--grub", dest="grub", action="store_true",
68 help="install grub bootloader instead of syslinux")
69 parser.add_option("--initrd", dest="initrd", action="store", type="string",
70 help="install specified initrd instead of the default")
71 parser.add_option("--kernel", dest="kernel", action="store", type="string",
72 help="install specified kernel instead of the default")
73 parser.add_option("--mbr", dest="mbr", action="store_true",
74 help="install master boot record (MBR) on the device")
75 parser.add_option("--quiet", dest="quiet", action="store_true",
76 help="do not output anything than errors on console")
77 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
78 help="install specified squashfs file instead of the default")
79 parser.add_option("--uninstall", dest="uninstall", action="store_true",
80 help="remove grml ISO files")
81 parser.add_option("--verbose", dest="verbose", action="store_true",
82 help="enable verbose mode")
83 parser.add_option("-v", "--version", dest="version", action="store_true",
84 help="display version and exit")
85 (options, args) = parser.parse_args()
92 logging.info("Cleaning up")
93 proc = subprocess.Popen(["sync"])
97 for device in mounted:
99 # ignore: RuntimeError: Set changed size during iteration
103 def get_function_name(obj):
104 if not (isroutine(obj) or isclass(obj)):
106 return obj.__module__ + '.' + obj.__name__
109 def execute(f, *args):
110 """Wrapper for executing a command. Either really executes
111 the command (default) or when using --dry-run commandline option
112 just displays what would be executed."""
113 # demo: execute(subprocess.Popen, (["ls", "-la"]))
115 logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, args))))
121 """Check whether a given file can be executed
123 @fpath: full path to file
125 return os.path.exists(fpath) and os.access(fpath, os.X_OK)
129 """Check whether a given program is available in PATH
131 @program: name of executable"""
132 fpath, fname = os.path.split(program)
137 for path in os.environ["PATH"].split(os.pathsep):
138 exe_file = os.path.join(path, program)
145 def search_file(filename, search_path='/bin' + pathsep + '/usr/bin'):
146 """Given a search path, find file"""
148 paths = search_path.split(pathsep)
150 for current_dir, directories, files in os.walk(path):
151 if exists(join(current_dir, filename)):
155 return abspath(join(current_dir, filename))
160 def check_uid_root():
161 """Check for root permissions"""
162 if not os.geteuid()==0:
163 sys.exit("Error: please run this script with uid 0 (root).")
166 def install_syslinux(device, dry_run=False):
168 """Install syslinux on specified device."""
170 # syslinux -d boot/isolinux /dev/sdb1
171 logging.info("Installing syslinux as bootloader")
172 logging.debug("syslinux -d boot/syslinux %s" % device)
173 proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
175 if proc.returncode != 0:
176 raise Exception, "error executing syslinux"
179 def generate_grub_config(grml_flavour):
180 """Generate grub configuration for use via menu,lst"""
183 # * install main part of configuration just *once* and append
184 # flavour specific configuration only
185 # * what about systems using grub2 without having grub1 available?
191 # color red/blue green/black
192 splashimage=/boot/grub/splash.xpm.gz
197 title %(grml_flavour)s - Default boot (using 1024x768 framebuffer)
198 kernel /boot/release/%(grml_flavour)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_flavour)s
199 initrd /boot/release/%(grml_flavour)s/initrd.gz
201 # TODO: extend configuration :)
205 def generate_isolinux_splash(grml_flavour):
206 """Generate bootsplash for isolinux/syslinux"""
209 # * adjust last bootsplash line
211 grml_name = grml_flavour
214 \ f17
\f\18/boot/syslinux/logo.16
216 Some information and boot options available via keys F2 - F10. http://grml.org/
220 def generate_main_syslinux_config(grml_flavour, grml_bootoptions):
221 """Generate main configuration for use in syslinux.cfg"""
224 # * install main part of configuration just *once* and append
225 # flavour specific configuration only
226 # * unify isolinux and syslinux setup ("INCLUDE /boot/...")
229 local_datestamp = datestamp
232 ## main syslinux configuration - generated by grml2usb [main config generated at: %(local_datestamp)s]
233 # use this to control the bootup via a serial port
238 DISPLAY /boot/syslinux/boot.msg
239 F1 /boot/syslinux/boot.msg
248 F10 /boot/syslinux/f10
249 ## end of main configuration
251 # flavour specific configuration for grml
253 KERNEL /boot/release/%(grml_flavour)s/linux26
254 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(grml_bootoptions)s
258 def generate_flavour_specific_syslinux_config(grml_flavour, bootoptions):
259 """Generate flavour specific configuration for use in syslinux.cfg"""
261 local_datestamp = datestamp
265 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
266 LABEL %(grml_flavour)s
267 KERNEL /boot/release/%(grml_flavour)s/linux26
268 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(bootoptions)s
272 def install_grub(device, dry_run=False):
273 """Install grub on specified device."""
274 logging.critical("TODO: grub-install %s" % device)
277 def install_bootloader(partition, dry_run=False):
278 """Install bootloader on device."""
280 # Install bootloader on the device (/dev/sda),
281 # not on the partition itself (/dev/sda1)?
282 # if partition[-1:].isdigit():
283 # device = re.match(r'(.*?)\d*$', partition).group(1)
288 install_grub(partition, dry_run)
290 install_syslinux(partition, dry_run)
293 def is_writeable(device):
294 """Check if the device is writeable for the current user"""
298 #raise Exception, "no device for checking write permissions"
300 if not os.path.exists(device):
303 return os.access(device, os.W_OK) and os.access(device, os.R_OK)
305 def install_mbr(device, dry_run=False):
306 """Install a default master boot record on given device
308 @device: device where MBR should be installed to"""
310 if not is_writeable(device):
311 raise IOError, "device not writeable for user"
313 lilo = '/grml/git/grml2usb/lilo/lilo.static' # FIXME
316 raise Exception, "lilo executable not available."
318 # to support -A for extended partitions:
319 logging.info("Installing MBR")
320 logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
321 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
323 if proc.returncode != 0:
324 raise Exception, "error executing lilo"
326 # activate partition:
327 logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
329 proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
331 if proc.returncode != 0:
332 raise Exception, "error executing lilo"
334 # lilo's mbr is broken, use the one from syslinux instead:
335 logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
338 # TODO -> use Popen instead?
339 retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
341 logging.critical("Error copying MBR to device (%s)" % retcode)
342 except OSError, error:
343 logging.critical("Execution failed:", error)
346 def register_tmpfile(path):
353 def unregister_tmpfile(path):
358 tmpfiles.remove(path)
361 def register_mountpoint(target):
368 def unregister_mountpoint(target):
372 if target in mounted:
373 mounted.remove(target)
376 def mount(source, target, options):
377 """Mount specified source on given target
379 @source: name of device/ISO that should be mounted
380 @target: directory where the ISO should be mounted to
381 @options: mount specific options"""
383 # notice: dry_run does not work here, as we have to locate files, identify flavour,...
384 logging.debug("mount %s %s %s" % (options, source, target))
385 proc = subprocess.Popen(["mount"] + list(options) + [source, target])
387 if proc.returncode != 0:
388 raise Exception, "Error executing mount"
390 logging.debug("register_mountpoint(%s)" % target)
391 register_mountpoint(target)
393 def unmount(target, options):
394 """Unmount specified target
396 @target: target where something is mounted on and which should be unmounted
397 @options: options for umount command"""
399 # make sure we unmount only already mounted targets
400 target_unmount = False
401 mounts = open('/proc/mounts').readlines()
402 mountstring = re.compile(".*%s.*" % re.escape(target))
404 if re.match(mountstring, line):
405 target_unmount = True
407 if not target_unmount:
408 logging.debug("%s not mounted anymore" % target)
410 logging.debug("umount %s %s" % (list(options), target))
411 proc = subprocess.Popen(["umount"] + list(options) + [target])
413 if proc.returncode != 0:
414 raise Exception, "Error executing umount"
416 logging.debug("unregister_mountpoint(%s)" % target)
417 unregister_mountpoint(target)
420 def check_for_usbdevice(device):
421 """Check whether the specified device is a removable USB device
423 @device: device name, like /dev/sda1 or /dev/sda
426 usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
427 usbdevice = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
428 if os.path.isfile(usbdevice):
429 is_usb = open(usbdevice).readline()
436 def check_for_fat(partition):
437 """Check whether specified partition is a valid VFAT/FAT16 filesystem
439 @partition: device name of partition"""
442 udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t", partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
443 filesystem = udev_info.communicate()[0].rstrip()
445 if udev_info.returncode == 2:
446 raise Exception, "Failed to read device %s - wrong UID / permissions?" % partition
448 if filesystem != "vfat":
449 raise Exception, "Device %s does not contain a FAT16 partition" % partition
452 raise Exception, "Sorry, /lib/udev/vol_id not available."
455 def mkdir(directory):
456 """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
458 if not os.path.isdir(directory):
460 os.makedirs(directory)
462 # just silently pass as it's just fine it the directory exists
466 def copy_grml_files(grml_flavour, iso_mount, target, dry_run=False):
467 """Copy files from ISO on given target"""
470 # * provide alternative search_file() if file information is stored in a config.ini file?
471 # * catch "install: .. No space left on device" & CO
472 # * abstract copy logic to make the code shorter and get rid of spaghetti ;)
474 if not options.bootloaderonly:
475 logging.info("Copying files. This might take a while....")
477 squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
478 squashfs_target = target + '/live/'
479 execute(mkdir, squashfs_target)
481 # use install(1) for now to make sure we can write the files afterwards as normal user as well
482 logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
483 proc = execute(subprocess.Popen, ["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
486 filesystem_module = search_file('filesystem.module', iso_mount)
487 logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
488 proc = execute(subprocess.Popen, ["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
491 release_target = target + '/boot/release/' + grml_flavour
492 execute(mkdir, release_target)
494 kernel = search_file('linux26', iso_mount)
495 logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
496 proc = execute(subprocess.Popen, ["install", "--mode=664", kernel, release_target + '/linux26'])
499 initrd = search_file('initrd.gz', iso_mount)
500 logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
501 proc = execute(subprocess.Popen, ["install", "--mode=664", initrd, release_target + '/initrd.gz'])
504 if not options.copyonly:
505 syslinux_target = target + '/boot/syslinux/'
506 execute(mkdir, syslinux_target)
508 logo = search_file('logo.16', iso_mount)
509 logging.debug("cp %s %s" % (logo, syslinux_target + 'logo.16'))
510 proc = execute(subprocess.Popen, ["install", "--mode=664", logo, syslinux_target + 'logo.16'])
513 for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
514 bootsplash = search_file(ffile, iso_mount)
515 logging.debug("cp %s %s" % (bootsplash, syslinux_target + ffile))
516 proc = execute(subprocess.Popen, ["install", "--mode=664", bootsplash, syslinux_target + ffile])
519 grub_target = target + '/boot/grub/'
520 execute(mkdir, grub_target)
522 logging.debug("cp /grml/git/grml2usb/grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz') # FIXME - path of grub
523 proc = execute(subprocess.Popen, ["install", "--mode=664", '/grml/git/grml2usb/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz']) # FIXME
526 logging.debug("cp /grml/git/grml2usb/grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito') # FIXME - path of grub
527 proc = execute(subprocess.Popen, ["install", "--mode=664", '/grml/git/grml2usb/grub/stage2_eltorito', grub_target + 'stage2_eltorito']) # FIXME
531 logging.debug("Generating grub configuration")
532 #with open("...", "w") as f:
533 #f.write("bla bla bal")
534 grub_config_file = open(grub_target + 'menu.lst', 'w')
535 grub_config_file.write(generate_grub_config(grml_flavour))
536 grub_config_file.close()
538 logging.info("Generating syslinux configuration")
539 syslinux_cfg = syslinux_target + 'syslinux.cfg'
541 # install main configuration only *once*, no matter how many ISOs we have:
542 if os.path.isfile(syslinux_cfg):
543 string = open(syslinux_cfg).readline()
544 main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(datestamp)))
545 if not re.match(main_identifier, string):
546 syslinux_config_file = open(syslinux_cfg, 'w')
547 logging.info("Notice: grml flavour %s is being installed as the default booting system." % grml_flavour)
548 syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
549 syslinux_config_file.close()
551 syslinux_config_file = open(syslinux_cfg, 'w')
552 syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
553 syslinux_config_file.close()
556 # install flavour specific configuration only *once* as well
557 # ugly - I'm pretty sure this could be smoother...
558 flavour_config = True
559 if os.path.isfile(syslinux_cfg):
560 string = open(syslinux_cfg).readlines()
561 logging.info("Notice: you can boot flavour %s using '%s' on the commandline." % (grml_flavour, grml_flavour))
562 flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(datestamp))))
564 if flavour.match(line):
565 flavour_config = False
569 syslinux_config_file = open(syslinux_cfg, 'a')
570 syslinux_config_file.write(generate_flavour_specific_syslinux_config(grml_flavour, options.bootoptions))
571 syslinux_config_file.close( )
573 logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
574 isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
575 isolinux_splash.write(generate_isolinux_splash(grml_flavour))
576 isolinux_splash.close( )
579 # make sure we sync filesystems before returning
580 proc = subprocess.Popen(["sync"])
583 def uninstall_files(device):
584 """Get rid of all grml files on specified device"""
587 logging.critical("TODO: %s" % device)
590 def identify_grml_flavour(mountpath):
591 """Get name of grml flavour
593 @mountpath: path where the grml ISO is mounted to
594 @return: name of grml-flavour"""
596 version_file = search_file('grml-version', mountpath)
598 if version_file == "":
599 logging.critical("Error: could not find grml-version file.")
603 tmpfile = open(version_file, 'r')
604 grml_info = tmpfile.readline()
605 grml_flavour = re.match(r'[\w-]*', grml_info).group()
609 logging.critical("Unexpected error:", sys.exc_info()[0])
614 def handle_iso(iso, device):
618 logging.info("Using ISO %s" % iso)
620 if os.path.isdir(iso):
621 logging.critical("TODO: /live/image handling not yet implemented") # TODO
623 iso_mountpoint = tempfile.mkdtemp()
624 register_tmpfile(iso_mountpoint)
625 remove_iso_mountpoint = True
627 mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
629 if os.path.isdir(device):
630 logging.info("Specified target is a directory, not mounting therefore.")
631 device_mountpoint = device
632 remove_device_mountpoint = False
636 device_mountpoint = tempfile.mkdtemp()
637 register_tmpfile(device_mountpoint)
638 remove_device_mountpoint = True
640 mount(device, device_mountpoint, "")
641 except Exception, error:
642 logging.critical("Fatal: %s" % error)
646 grml_flavour = identify_grml_flavour(iso_mountpoint)
647 logging.info("Identified grml flavour \"%s\"." % grml_flavour)
648 copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint, dry_run=options.dryrun)
650 logging.critical("Fatal: a critical error happend during execution, giving up")
653 if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
654 unmount(iso_mountpoint, "")
656 os.rmdir(iso_mountpoint)
657 unregister_tmpfile(iso_mountpoint)
659 if remove_device_mountpoint:
660 unmount(device_mountpoint, "")
662 if os.path.isdir(device_mountpoint):
663 os.rmdir(device_mountpoint)
664 unregister_tmpfile(device_mountpoint)
666 # grml_flavour_short = grml_flavour.replace('-','')
667 # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
671 """Main function [make pylint happy :)]"""
674 print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
678 parser.error("invalid usage")
682 FORMAT = "%(asctime)-15s %(message)s"
683 logging.basicConfig(level=logging.DEBUG, format=FORMAT)
685 FORMAT = "Critial: %(message)s"
686 logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
688 FORMAT = "Info: %(message)s"
689 logging.basicConfig(level=logging.INFO, format=FORMAT)
692 logging.info("Running in simulate mode as requested via option dry-run.")
696 # specified arguments
697 device = args[len(args) - 1]
698 isos = args[0:len(args) - 1]
700 # make sure we can replace old grml2usb script and warn user when using old way of life:
701 if device.startswith("/mnt/external") or device.startswith("/mnt/usb"):
702 print "Warning: the semantics of grml2usb has changed."
703 print "Instead of using grml2usb /path/to/iso %s you might" % device
704 print "want to use grml2usb /path/to/iso /dev/... instead."
705 print "Please check out the grml2usb manpage for details."
706 f = raw_input("Do you really want to continue? y/N ")
707 if f == "y" or f == "Y":
712 # make sure we have syslinux available
713 if not which("syslinux") and not options.copyonly:
714 logging.critical('Sorry, syslinux not available. Exiting.')
715 logging.critical('Please install syslinux or consider using the --grub option.')
718 # check for vfat filesystem
719 if device is not None and not os.path.isdir(device):
721 check_for_fat(device)
722 except Exception, error:
723 logging.critical("Execution failed: %s", error)
726 if not check_for_usbdevice(device):
727 print "Warning: the specified device %s does not look like a removable usb device." % device
728 f = raw_input("Do you really want to continue? y/N ")
729 if f == "y" or f == "Y":
734 # main operation (like installing files)
736 handle_iso(iso, device)
739 if not options.mbr or skip_mbr:
740 logging.info("You are not using the --mbr option. Consider using it to get a working USB setup.")
742 # make sure we install MBR on /dev/sdX and not /dev/sdX#
743 if device[-1:].isdigit():
744 mbr_device = re.match(r'(.*?)\d*$', device).group(1)
747 install_mbr(mbr_device, dry_run=options.dryrun)
748 except IOError, error:
749 logging.critical("Execution failed: %s", error)
751 except Exception, error:
752 logging.critical("Execution failed: %s", error)
755 # Install bootloader only if not using the --copy-only option
757 logging.info("Not installing bootloader and its files as requested via option copyonly.")
759 install_bootloader(device, dry_run=options.dryrun)
761 # finally be politely :)
762 logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
764 if __name__ == "__main__":
767 except KeyboardInterrupt:
768 logging.info("Received KeyboardInterrupt")
771 ## END OF FILE #################################################################
772 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8