0a34583c9b17e3b9074c9cbb1936a93e34f0627a
[grml2usb.git] / grml2usb
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # pylint: disable-msg=C0302
4 """
5 grml2usb
6 ~~~~~~~~
7
8 This script installs a grml system (either a running system or ISO[s]) to a USB device
9
10 :copyright: (c) 2009, 2010, 2011 by Michael Prokop <mika@grml.org>
11 :license: GPL v2 or any later version
12 :bugreports: http://grml.org/bugs/
13
14 """
15
16 from optparse import OptionParser
17 from inspect import isroutine, isclass
18 import datetime
19 import fileinput
20 import glob
21 import logging
22 import os
23 import os.path
24 import re
25 import struct
26 import subprocess
27 import sys
28 import tempfile
29 import time
30 import uuid
31
32 # The line following this line is patched by debian/rules and tarball.sh.
33 PROG_VERSION = '***UNRELEASED***'
34
35 # global variables
36 MOUNTED = set()   # register mountpoints
37 TMPFILES = set()  # register tmpfiles
38 DATESTAMP = time.mktime(datetime.datetime.now().timetuple())  # unique identifier for syslinux.cfg
39 GRML_FLAVOURS = set()  # which flavours are being installed?
40 GRML_DEFAULT = None
41 UUID = None
42 SYSLINUX_LIBS = "/usr/lib/syslinux/"
43
44
45 def syslinux_warning(option, opt, value, opt_parser):
46     """A helper function for printing a warning about deprecated option
47     """
48     # pylint: disable-msg=W0613
49     sys.stderr.write("Note: the --syslinux option is deprecated as syslinux "
50                      "is grml2usb's default. Continuing anyway.\n")
51     setattr(opt_parser.values, option.dest, True)
52
53
54 # if grub option is set, unset syslinux option
55 def grub_option(option, opt, value, opt_parser):
56     """A helper function adjusting other option values
57     """
58     # pylint: disable-msg=W0613
59     setattr(opt_parser.values, option.dest, True)
60     setattr(opt_parser.values, 'syslinux', False)
61
62 # cmdline parsing
63 USAGE = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/sdX#>\n\
64 \n\
65 %prog installs grml ISO[s] to an USB device to be able to boot from it.\n\
66 Make sure you have at least one grml ISO or a running grml system (/live/image),\n\
67 grub or syslinux and root access.\n\
68 \n\
69 Run %prog --help for usage hints, further information via: man grml2usb"
70
71 # pylint: disable-msg=C0103
72 # pylint: disable-msg=W0603
73 parser = OptionParser(usage=USAGE)
74 parser.add_option("--bootoptions", dest="bootoptions",
75                   action="append", type="string",
76                   help="use specified bootoptions as default")
77 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
78                   help="do not copy files but just install a bootloader")
79 parser.add_option("--copy-only", dest="copyonly", action="store_true",
80                   help="copy files only but do not install bootloader")
81 parser.add_option("--dry-run", dest="dryrun", action="store_true",
82                   help="avoid executing commands")
83 parser.add_option("--fat16", dest="fat16", action="store_true",
84                   help="format specified partition with FAT16")
85 parser.add_option("--force", dest="force", action="store_true",
86                   help="force any actions requiring manual interaction")
87 parser.add_option("--grub", dest="grub", action="callback",
88                   callback=grub_option,
89                   help="install grub bootloader instead of (default) syslinux")
90 parser.add_option("--grub-mbr", dest="grubmbr", action="store_true",
91                   help="install grub into MBR instead of (default) PBR")
92 parser.add_option("--mbr-menu", dest="mbrmenu", action="store_true",
93                   help="enable interactive boot menu in MBR")
94 parser.add_option("--quiet", dest="quiet", action="store_true",
95                   help="do not output anything but just errors on console")
96 parser.add_option("--remove-bootoption", dest="removeoption", action="append",
97                   help="regex for removing existing bootoptions")
98 parser.add_option("--skip-addons", dest="skipaddons", action="store_true",
99                   help="do not install /boot/addons/ files")
100 parser.add_option("--skip-grub-config", dest="skipgrubconfig", action="store_true",
101                   help="skip generation of grub configuration files")
102 parser.add_option("--skip-mbr", dest="skipmbr", action="store_true",
103                   help="do not install a master boot record (MBR) on the device")
104 parser.add_option("--skip-syslinux-config", dest="skipsyslinuxconfig", action="store_true",
105                   help="skip generation of syslinux configuration files")
106 parser.add_option("--syslinux", dest="syslinux", action="callback", default=True,
107                   callback=syslinux_warning,
108                   help="install syslinux bootloader (deprecated as it's the default)")
109 parser.add_option("--syslinux-mbr", dest="syslinuxmbr", action="store_true",
110                   help="install syslinux master boot record (MBR) instead of default")
111 parser.add_option("--verbose", dest="verbose", action="store_true",
112                   help="enable verbose mode")
113 parser.add_option("-v", "--version", dest="version", action="store_true",
114                   help="display version and exit")
115 (options, args) = parser.parse_args()
116
117
118 GRML2USB_BASE = '/usr/share/grml2usb'
119 if not os.path.isdir(GRML2USB_BASE):
120     GRML2USB_BASE = os.path.dirname(os.path.realpath(__file__))
121
122
123 class CriticalException(Exception):
124     """Throw critical exception if the exact error is not known but fatal.
125
126     @Exception: message"""
127     pass
128
129
130 # The following two functions help to operate on strings as
131 # array (list) of bytes (octets). In Python 3000, the bytes
132 # datatype will need to be used. This is intended for using
133 # with manipulation of files on the octet level, like shell
134 # arrays, e.g. in MBR creation.
135
136
137 def array2string(*a):
138     """Convert a list of integers [0;255] to a string."""
139     return struct.pack("%sB" % len(a), *a)
140
141
142 def string2array(s):
143     """Convert a (bytes) string into a list of integers."""
144     return struct.unpack("%sB" % len(s), s)
145
146
147 def cleanup():
148     """Cleanup function to make sure there aren't any mounted devices left behind.
149     """
150
151     logging.info("Cleaning up before exiting...")
152     proc = subprocess.Popen(["sync"])
153     proc.wait()
154
155     try:
156         for device in MOUNTED:
157             unmount(device, "")
158         for tmpfile in TMPFILES:
159             os.unlink(tmpfile)
160     # ignore: RuntimeError: Set changed size during iteration
161     except RuntimeError:
162         logging.debug('caught expection RuntimeError, ignoring')
163
164
165 def register_tmpfile(path):
166     """
167     register tmpfile
168     """
169
170     TMPFILES.add(path)
171
172
173 def unregister_tmpfile(path):
174     """
175     remove registered tmpfile
176     """
177
178     try:
179         TMPFILES.remove(path)
180     except KeyError:
181         pass
182
183
184 def register_mountpoint(target):
185     """register specified target in a set() for handling clean exiting
186
187     @target: destination target of mountpoint
188     """
189
190     MOUNTED.add(target)
191
192
193 def unregister_mountpoint(target):
194     """unregister specified target in a set() for handling clean exiting
195
196     @target: destination target of mountpoint
197     """
198
199     if target in MOUNTED:
200         MOUNTED.remove(target)
201
202
203 def get_function_name(obj):
204     """Helper function for use in execute() to retrive name of a function
205
206     @obj: the function object
207     """
208     if not (isroutine(obj) or isclass(obj)):
209         obj = type(obj)
210     return obj.__module__ + '.' + obj.__name__
211
212
213 def execute(f, *exec_arguments):
214     """Wrapper for executing a command. Either really executes
215     the command (default) or when using --dry-run commandline option
216     just displays what would be executed."""
217     # usage: execute(subprocess.Popen, (["ls", "-la"]))
218     if options.dryrun:
219         # pylint: disable-msg=W0141
220         logging.debug('dry-run only: %s(%s)', get_function_name(f), ', '.join(map(repr, exec_arguments)))
221     else:
222         # pylint: disable-msg=W0142
223         return f(*exec_arguments)
224
225
226 def is_exe(fpath):
227     """Check whether a given file can be executed
228
229     @fpath: full path to file
230     @return:"""
231     return os.path.exists(fpath) and os.access(fpath, os.X_OK)
232
233
234 def which(program):
235     """Check whether a given program is available in PATH
236
237     @program: name of executable"""
238     fpath = os.path.split(program)[0]
239     if fpath:
240         if is_exe(program):
241             return program
242     else:
243         for path in os.environ["PATH"].split(os.pathsep):
244             exe_file = os.path.join(path, program)
245             if is_exe(exe_file):
246                 return exe_file
247
248     return None
249
250
251 def get_defaults_file(iso_mount, flavour, name):
252     """get the default file for syslinux
253     """
254     bootloader_dirs = ['/boot/isolinux/', '/boot/syslinux/']
255     for directory in bootloader_dirs:
256         for name in name, \
257         "%s_%s" % (get_flavour_filename(flavour), name):
258             if os.path.isfile(iso_mount + directory + name):
259                 return (directory, name)
260     return ('', '')
261
262
263 def search_file(filename, search_path='/bin' + os.pathsep + '/usr/bin', lst_return=False):
264     """Given a search path, find file
265
266     @filename: name of file to search for
267     @search_path: path where searching for the specified filename
268     @lst_return: return list of matching files instead one file"""
269     paths = search_path.split(os.pathsep)
270     current_dir = ''  # make pylint happy :)
271     retval = []
272
273     def match_file(cwd):
274         """Helper function ffor testing if specified file exists in cwd
275
276         @cwd: current working directory
277         """
278         return  os.path.exists(os.path.join(cwd, filename))
279
280     for path in paths:
281         current_dir = path
282         if match_file(current_dir):
283             retval.append(os.path.abspath(os.path.join(current_dir, filename)))
284             if not lst_return:
285                 break
286         # pylint: disable-msg=W0612
287         for current_dir, directories, files in os.walk(path):
288             if match_file(current_dir):
289                 retval.append(os.path.abspath(os.path.join(current_dir, filename)))
290                 if not lst_return:
291                     break
292     if lst_return:
293         return retval
294     elif retval:
295         return retval[0]
296     else:
297         return None
298
299
300 def check_uid_root():
301     """Check for root permissions"""
302     if not os.geteuid() == 0:
303         sys.exit("Error: please run this script with uid 0 (root).")
304
305
306 def mkfs_fat16(device):
307     """Format specified device with VFAT/FAT16 filesystem.
308
309     @device: partition that should be formated"""
310
311     if options.dryrun:
312         logging.info("Would execute mkfs.vfat -F 16 %s now.", device)
313         return 0
314
315     logging.info("Formating partition with fat16 filesystem")
316     logging.debug("mkfs.vfat -F 16 %s", device)
317     proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
318     proc.wait()
319     if proc.returncode != 0:
320         raise CriticalException("error executing mkfs.vfat")
321
322
323 def generate_isolinux_splash(grml_flavour):
324     """Generate bootsplash for isolinux/syslinux
325
326     @grml_flavour: name of grml flavour the configuration should be generated for"""
327
328     grml_name = grml_flavour
329
330     return("""\
331 \ f17\f\18/boot/syslinux/logo.16
332
333 Some information and boot options available via keys F2 - F10. http://grml.org/
334 %(grml_name)s
335 """ % {'grml_name': grml_name})
336
337
338 def generate_main_syslinux_config(*arg):
339     """Generate main configuration for use in syslinux.cfg
340
341     @*arg: just for backward compatibility"""
342     # pylint: disable-msg=W0613
343     # remove warning about unused arg
344
345     return("""\
346 label -
347 menu label Default boot modes:
348 menu disable
349 include defaults.cfg
350
351 menu end
352 menu separator
353
354 # flavours:
355 label -
356 menu label Additional boot entries for:
357 menu disable
358 include additional.cfg
359
360 menu separator
361 include options.cfg
362 include addons.cfg
363
364 label help
365   include promptname.cfg
366   config prompt.cfg
367   text help
368                                         Jump to old style isolinux prompt
369                                         featuring further information
370                                         regarding available boot options.
371   endtext
372
373
374 include hiddens.cfg
375 """)
376
377
378 def generate_flavour_specific_syslinux_config(grml_flavour):
379     """Generate flavour specific configuration for use in syslinux.cfg
380
381     @grml_flavour: name of grml flavour the configuration should be generated for"""
382
383     return("""\
384 menu begin grml %(grml_flavour)s
385     menu title %(display_name)s
386     label mainmenu
387     menu label ^Back to main menu...
388     menu exit
389     menu separator
390     # include config for boot parameters from disk
391     include %(grml_flavour)s_grml.cfg
392     menu hide
393 menu end
394 """ % {'grml_flavour': grml_flavour, 'display_name': get_flavour_filename(grml_flavour)})
395
396
397 def install_grub(device):
398     """Install grub on specified device.
399
400     @mntpoint: mountpoint of device where grub should install its files to
401     @device: partition where grub should be installed to"""
402
403     if options.dryrun:
404         logging.info("Would execute grub-install [--root-directory=mount_point] %s now.", device)
405     else:
406         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
407         register_tmpfile(device_mountpoint)
408         try:
409             try:
410                 mount(device, device_mountpoint, "")
411
412                 # If using --grub-mbr then make sure we install grub in MBR instead of PBR
413                 if options.grubmbr:
414                     logging.debug("Using option --grub-mbr ...")
415                     if device[-1:].isdigit():
416                         grub_device = re.match(r'(.*?)\d*$', device).group(1)
417                     else:
418                         grub_device = device
419                 else:
420                     grub_device = device
421
422                 logging.info("Installing grub as bootloader")
423                 for opt in ["", "--force"]:
424                     logging.debug("grub-install --recheck %s --no-floppy --root-directory=%s %s",
425                                   opt, device_mountpoint, grub_device)
426                     proc = subprocess.Popen(["grub-install", "--recheck", opt, "--no-floppy",
427                                              "--root-directory=%s" % device_mountpoint, grub_device],
428                                             stdout=file(os.devnull, "r+"))
429                     proc.wait()
430                     if proc.returncode == 0:
431                         break
432
433                 if proc.returncode != 0:
434                     # raise Exception("error executing grub-install")
435                     logging.critical("Fatal: error executing grub-install "
436                                      + "(please check the grml2usb FAQ or drop the --grub option)")
437                     logging.critical("Note:  if using grub2 consider using "
438                                      + "the --grub-mbr option as grub considers PBR problematic.")
439                     cleanup()
440                     sys.exit(1)
441             except CriticalException, error:
442                 logging.critical("Fatal: %s", error)
443                 cleanup()
444                 sys.exit(1)
445
446         finally:
447             unmount(device_mountpoint, "")
448             os.rmdir(device_mountpoint)
449             unregister_tmpfile(device_mountpoint)
450
451
452 def install_syslinux(device):
453     """Install syslinux on specified device.
454
455     @device: partition where syslinux should be installed to"""
456
457     if options.dryrun:
458         logging.info("Would install syslinux as bootloader on %s", device)
459         return 0
460
461     # syslinux -d boot/isolinux /dev/sdb1
462     logging.info("Installing syslinux as bootloader")
463     logging.debug("syslinux -d boot/syslinux %s", device)
464     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
465     proc.wait()
466     if proc.returncode != 0:
467         raise CriticalException("Error executing syslinux (either try --fat16 or use grub?)")
468
469
470 def install_bootloader(device):
471     """Install bootloader on specified device.
472
473     @device: partition where bootloader should be installed to"""
474
475     # by default we use grub, so install syslinux only on request
476     if options.grub:
477         try:
478             install_grub(device)
479         except CriticalException, error:
480             logging.critical("Fatal: %s", error)
481             cleanup()
482             sys.exit(1)
483     else:
484         try:
485             install_syslinux(device)
486         except CriticalException, error:
487             logging.critical("Fatal: %s", error)
488             cleanup()
489             sys.exit(1)
490
491
492 def install_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
493     """install 'mbr' master boot record (MBR) on a device
494
495     Retrieve the partition table from "device", install an MBR from the
496     "mbrtemplate" file, set the "partition" (0..3) active, and install the
497     result back to "device".
498
499     @mbrtemplate: default MBR file
500
501     @device: name of a file assumed to be a hard disc (or USB stick) image, or
502     something like "/dev/sdb"
503
504     @partition: must be a number between 0 and 3, inclusive
505
506     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
507     ismirbsdmbr) bytes.
508
509     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
510     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
511     false then leave the mirbsdmbr specific flag set to FFh, set all
512     active flags to 0 and set the active flag of the partition to 80h.  Note:
513     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
514     the active flag."""
515
516     logging.info("Installing default MBR")
517
518     if not os.path.isfile(mbrtemplate):
519         logging.critical("Error: %s can not be read.", mbrtemplate)
520         raise CriticalException("Error installing MBR (either try --syslinux-mbr or install missing file \"%s\"?)" % mbrtemplate)
521
522     if partition is not None and ((partition < 0) or (partition > 3)):
523         logging.warn("Cannot activate partition %d" % partition)
524         partition = None
525
526     if ismirbsdmbr:
527         nmbrbytes = 439
528     else:
529         nmbrbytes = 440
530
531     tmpf = tempfile.NamedTemporaryFile()
532
533     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
534     proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
535                             stderr=file(os.devnull, "r+"))
536     proc.wait()
537     if proc.returncode != 0:
538         raise Exception("error executing dd (first run)")
539
540     logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc", mbrtemplate,
541                   tmpf.name, nmbrbytes)
542     proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes,
543                              "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
544     proc.wait()
545     if proc.returncode != 0:
546         raise Exception("error executing dd (second run)")
547
548     mbrcode = tmpf.file.read(512)
549     if len(mbrcode) < 512:
550         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
551
552     if partition is not None:
553         if ismirbsdmbr:
554             mbrcode = mbrcode[0:439] + chr(partition) + \
555                     mbrcode[440:510] + "\x55\xAA"
556         else:
557             actives = ["\x00", "\x00", "\x00", "\x00"]
558             actives[partition] = "\x80"
559             mbrcode = mbrcode[0:446] + actives[0] + \
560                     mbrcode[447:462] + actives[1] + \
561                     mbrcode[463:478] + actives[2] + \
562                     mbrcode[479:494] + actives[3] + \
563                     mbrcode[495:510] + "\x55\xAA"
564
565     tmpf.file.seek(0)
566     tmpf.file.truncate()
567     tmpf.file.write(mbrcode)
568     tmpf.file.close()
569
570     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device)
571     proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % device, "bs=512", "count=1",
572                              "conv=notrunc"], stderr=file(os.devnull, "r+"))
573     proc.wait()
574     if proc.returncode != 0:
575         raise Exception("error executing dd (third run)")
576     del tmpf
577
578
579 def is_writeable(device):
580     """Check if the device is writeable for the current user
581
582     @device: partition where bootloader should be installed to"""
583
584     if not device:
585         return False
586         #raise Exception("no device for checking write permissions")
587
588     if not os.path.exists(device):
589         return False
590
591     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
592
593
594 def mount(source, target, mount_options):
595     """Mount specified source on given target
596
597     @source: name of device/ISO that should be mounted
598     @target: directory where the ISO should be mounted to
599     @options: mount specific options"""
600
601     # note: options.dryrun does not work here, as we have to
602     # locate files and identify the grml flavour
603
604     for x in file('/proc/mounts').readlines():
605         if x.startswith(source):
606             raise CriticalException("Error executing mount: %s already mounted - " % source
607                                     + "please unmount before invoking grml2usb")
608
609     if os.path.isdir(source):
610         logging.debug("Source %s is not a device, therefore not mounting.", source)
611         return 0
612
613     logging.debug("mount %s %s %s", mount_options, source, target)
614     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
615     proc.wait()
616     if proc.returncode != 0:
617         raise CriticalException("Error executing mount (no filesystem on the partition?)")
618     else:
619         logging.debug("register_mountpoint(%s)", target)
620         register_mountpoint(target)
621
622
623 def unmount(target, unmount_options):
624     """Unmount specified target
625
626     @target: target where something is mounted on and which should be unmounted
627     @options: options for umount command"""
628
629     # make sure we unmount only already mounted targets
630     target_unmount = False
631     mounts = open('/proc/mounts').readlines()
632     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
633     for line in mounts:
634         if re.match(mountstring, line):
635             target_unmount = True
636
637     if not target_unmount:
638         logging.debug("%s not mounted anymore", target)
639     else:
640         logging.debug("umount %s %s", list(unmount_options), target)
641         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
642         proc.wait()
643         if proc.returncode != 0:
644             raise Exception("Error executing umount")
645         else:
646             logging.debug("unregister_mountpoint(%s)", target)
647             unregister_mountpoint(target)
648
649
650 def check_for_usbdevice(device):
651     """Check whether the specified device is a removable USB device
652
653     @device: device name, like /dev/sda1 or /dev/sda
654     """
655
656     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
657     # newer systems:
658     usbdev = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
659     if not os.path.isfile(usbdev):
660         # Ubuntu with kernel 2.6.24 for example:
661         usbdev = os.path.realpath('/sys/block/' + usbdevice + '/removable')
662
663     if os.path.isfile(usbdev):
664         is_usb = open(usbdev).readline()
665         if is_usb.find("1"):
666             return 0
667
668     return 1
669
670
671 def check_for_fat(partition):
672     """Check whether specified partition is a valid VFAT/FAT16 filesystem
673
674     @partition: device name of partition"""
675
676     if not os.access(partition, os.R_OK):
677         raise CriticalException("Failed to read device %s"
678                 " (wrong UID/permissions or device/directory not present?)" % partition)
679
680     try:
681         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
682                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
683         filesystem = udev_info.communicate()[0].rstrip()
684
685         if filesystem != "vfat":
686             raise CriticalException(
687                     "Partition %s does not contain a FAT16 filesystem. "
688                     "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
689
690     except OSError:
691         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
692
693
694 def mkdir(directory):
695     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
696
697     # just silently pass as it's just fine it the directory exists
698     if not os.path.isdir(directory):
699         try:
700             os.makedirs(directory)
701         # pylint: disable-msg=W0704
702         except OSError:
703             pass
704
705
706 def exec_rsync(source, target):
707     """Simple wrapper around rsync to install files
708
709     @source: source file/directory
710     @target: target file/directory"""
711     logging.debug("Source: %s / Target: %s", source, target)
712     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
713     proc.wait()
714     if proc.returncode == 12:
715         logging.critical("Fatal: No space left on device")
716         cleanup()
717         sys.exit(1)
718
719     if proc.returncode != 0:
720         logging.critical("Fatal: could not install %s", source)
721         cleanup()
722         sys.exit(1)
723
724
725 def write_uuid(target_file):
726     """Generates an returns uuid and write it to the specified file
727
728     @target_file: filename to write the uuid to
729     """
730
731     fileh = open(target_file, 'w')
732     uid = str(uuid.uuid4())
733     fileh.write(uid)
734     fileh.close()
735     return uid
736
737
738 def get_uuid(target):
739     """Get the uuid of the specified target. Will generate an uuid if none exist.
740
741     @target: directory/mountpoint containing the grml layout
742     """
743
744     conf_target = target + "/conf/"
745     uuid_file_name = conf_target + "/bootid.txt"
746     if os.path.isdir(conf_target):
747         if os.path.isfile(uuid_file_name):
748             uuid_file = open(uuid_file_name, 'r')
749             uid = uuid_file.readline().strip()
750             uuid_file.close()
751             return uid
752         else:
753             return write_uuid(uuid_file_name)
754     else:
755         execute(mkdir, conf_target)
756         return write_uuid(uuid_file_name)
757
758
759 def get_shortname(grml_flavour):
760     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
761     @grml_flavour: flavour name which shold be translated to shortname"""
762
763     return re.sub(r'[,._-]', '', grml_flavour)
764
765
766 def copy_system_files(grml_flavour, iso_mount, target):
767     """copy grml's main files (like squashfs, kernel and initrd) to a given target
768
769     @grml_flavour: name of grml flavour the configuration should be generated for
770     @iso_mount: path where a grml ISO is mounted on
771     @target: path where grml's main files should be copied to"""
772
773     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
774     if squashfs is None:
775         logging.critical("Fatal: squashfs file not found"
776         ", please check that your iso is not corrupt")
777         raise CriticalException("error locating squashfs file")
778     else:
779         squashfs_target = target + '/live/' + grml_flavour + '/'
780         execute(mkdir, squashfs_target)
781     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
782
783     for prefix in grml_flavour + "/", "":
784         filesystem_module = search_file(prefix + 'filesystem.module', iso_mount)
785         if filesystem_module:
786             break
787     if filesystem_module is None:
788         logging.critical("Fatal: filesystem.module not found")
789         raise CriticalException("error locating filesystem.module file")
790     else:
791         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
792
793     shortname = get_shortname(grml_flavour)
794     if os.path.isdir(iso_mount + '/boot/' + shortname):
795         exec_rsync(iso_mount + '/boot/' + shortname, target + '/boot')
796     else:
797         kernel = search_file('vmlinuz', iso_mount)
798         if kernel is None:
799             # compat for releases < 2011.12
800             kernel = search_file('linux26', iso_mount)
801
802         if kernel is None:
803             logging.critical("Fatal: kernel not found")
804             raise CriticalException("error locating kernel file")
805
806         source = os.path.dirname(kernel) + '/'
807         dest = target + '/' + os.path.dirname(kernel).replace(iso_mount, '') + '/'
808         execute(mkdir, dest)
809         exec_rsync(source, dest)
810
811
812 def update_grml_versions(iso_mount, target):
813     """Update the grml version file on a cd
814     Returns true if version was updated successfully,
815     False if grml-version does not exist yet on the mountpoint
816
817     @iso_mount: string of the iso mount point
818     @target: path of the target mount point
819     """
820     grml_target = target + '/grml/'
821     target_grml_version_file = search_file('grml-version', grml_target)
822     if target_grml_version_file:
823         iso_grml_version_file = search_file('grml-version', iso_mount)
824         if not iso_grml_version_file:
825             logging.warn("Warning: %s could not be found - can not install it", iso_grml_version_file)
826             return False
827         try:
828             # read the flavours from the iso image
829             iso_versions = {}
830             iso_file = open(iso_grml_version_file, 'r')
831             for line in iso_file:
832                 iso_versions[get_flavour(line)] = line.strip()
833
834             # update the existing flavours on the target
835             for line in fileinput.input([target_grml_version_file], inplace=1):
836                 flavour = get_flavour(line)
837                 if flavour in iso_versions.keys():
838                     print iso_versions.pop(flavour)
839                 else:
840                     print line.strip()
841             fileinput.close()
842
843             target_file = open(target_grml_version_file, 'a')
844             # add the new flavours from the current iso
845             for flavour in iso_versions:
846                 target_file.write("%s\n" % iso_versions[flavour])
847         except IOError:
848             logging.warn("Warning: Could not write file")
849         finally:
850             iso_file.close()
851             target_file.close()
852         return True
853     else:
854         return False
855
856
857 def copy_grml_files(grml_flavour, iso_mount, target):
858     """copy some minor grml files to a given target
859
860     @grml_flavour: the current grml_flavour
861     @iso_mount: path where a grml ISO is mounted on
862     @target: path where grml's main files should be copied to"""
863
864     grml_target = target + '/grml/'
865     execute(mkdir, grml_target)
866
867     grml_prefixe = ["GRML", "grml"]
868     for prefix in grml_prefixe:
869         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
870         if os.path.exists(filename):
871             exec_rsync(filename, grml_target)
872             break
873     else:
874         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
875
876
877 def handle_addon_copy(filename, dst, iso_mount, ignore_errors=False):
878     """handle copy of optional addons
879
880     @filename: filename of the addon
881     @dst: destination directory
882     @iso_mount: location of the iso mount
883     @ignore_errors: don't report missing files
884     """
885     file_location = search_file(filename, iso_mount)
886     if file_location is None:
887         if not ignore_errors:
888             logging.warn("Warning: %s not found (that's fine if you don't need it)", filename)
889     else:
890         exec_rsync(file_location, dst)
891
892
893 def copy_addons(iso_mount, target):
894     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
895
896     @iso_mount: path where a grml ISO is mounted on
897     @target: path where grml's main files should be copied to"""
898
899     addons = target + '/boot/addons/'
900     execute(mkdir, addons)
901
902     # grub all-in-one image
903     handle_addon_copy('allinone.img', addons, iso_mount)
904
905     # bsd imag
906     handle_addon_copy('bsd4grml', addons, iso_mount)
907
908     handle_addon_copy('balder10.imz', addons, iso_mount)
909
910     # install hdt and pci.ids only when using syslinux (grub doesn't support it)
911     if options.syslinux:
912         # hdt (hardware detection tool) image
913         hdtimg = search_file('hdt.c32', iso_mount)
914         if hdtimg:
915             exec_rsync(hdtimg, addons + '/hdt.c32')
916
917         # pci.ids file
918         picids = search_file('pci.ids', iso_mount)
919         if picids:
920             exec_rsync(picids, addons + '/pci.ids')
921
922     # memdisk image
923     handle_addon_copy('memdisk', addons, iso_mount)
924
925     # memtest86+ image
926     handle_addon_copy('memtest', addons, iso_mount)
927
928     # gpxe.lkrn: got replaced by ipxe
929     handle_addon_copy('gpxe.lkrn', addons, iso_mount, ignore_errors=True)
930
931     # ipxe.lkrn
932     handle_addon_copy('ipxe.lkrn', addons, iso_mount)
933
934
935 def build_loopbackcfg(target):
936     """Generate GRUB's loopback.cfg based on existing config files.
937
938     @target: target directory
939     """
940
941     grub_dir = '/boot/grub/'
942     mkdir(os.path.join(target, grub_dir))
943
944     f = open(target + grub_dir + 'loopback.cfg', 'w')
945
946     f.write("# grml2usb generated grub2 configuration file\n")
947     f.write("source /boot/grub/header.cfg\n")
948
949     for defaults in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"):
950         sourcefile = defaults.split(target + os.path.sep)[1]
951         logging.debug("Found source file" + sourcefile)
952         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
953
954     for ops in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"):
955         sourcefile = ops.split(target + os.path.sep)[1]
956         logging.debug("Found source file" + sourcefile)
957         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
958
959     f.write("source /boot/grub/adddons.cfg\n")
960     f.write("source /boot/grub/footer.cfg\n")
961     f.close()
962
963
964 def glob_and_copy(filepattern, dst):
965     """Glob on specified filepattern and copy the result to dst
966
967     @filepattern: globbing pattern
968     @dst: target directory
969     """
970     for name in glob.glob(filepattern):
971         copy_if_exist(name, dst)
972
973
974 def search_and_copy(filename, search_path, dst):
975     """Search for the specified filename at searchpath and copy it to dst
976
977     @filename: filename to look for
978     @search_path: base search file
979     @dst: destionation to copy the file to
980     """
981     file_location = search_file(filename, search_path)
982     copy_if_exist(file_location, dst)
983
984
985 def copy_if_exist(filename, dst):
986     """Copy filename to dst if filename is set.
987
988     @filename: a filename
989     @dst: dst file
990     """
991     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
992         exec_rsync(filename, dst)
993
994
995 def copy_bootloader_files(iso_mount, target, grml_flavour):
996     """Copy grml's bootloader files to a given target
997
998     @iso_mount: path where a grml ISO is mounted on
999     @target: path where grml's main files should be copied to
1000     @grml_flavour: name of the current processed grml_flavour
1001     """
1002
1003     syslinux_target = target + '/boot/syslinux/'
1004     execute(mkdir, syslinux_target)
1005
1006     grub_target = target + '/boot/grub/'
1007     execute(mkdir, grub_target)
1008
1009     logo = search_file('logo.16', iso_mount)
1010     exec_rsync(logo, syslinux_target + 'logo.16')
1011
1012     bootx64_efi = search_file('bootx64.efi', iso_mount)
1013     if bootx64_efi:
1014         mkdir(target + '/efi/boot/')
1015         exec_rsync(bootx64_efi, target + '/efi/boot/bootx64.efi')
1016
1017     efi_img = search_file('efi.img', iso_mount)
1018     if efi_img:
1019         mkdir(target + '/boot/')
1020         exec_rsync(efi_img, target + '/boot/efi.img')
1021
1022     for ffile in ['f%d' % number for number in range(1, 11)]:
1023         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1024
1025     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1026     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1027     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1028         os.unlink(syslinux_target + 'ldlinux.sys')
1029
1030     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1031     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1032
1033     if not source_dir:
1034         logging.critical("Fatal: file default.cfg could not be found.")
1035         logging.critical("Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...")
1036         logging.critical("       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1037         raise
1038
1039     if not os.path.exists(iso_mount + '/boot/grub/footer.cfg'):
1040         logging.warning("Warning: Grml releases older than 2011.12 support only one flavour in grub.")
1041
1042     for expr in name, 'distri.cfg', \
1043         defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1044         'isoprompt.cfg', 'options.cfg', \
1045         'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1046         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1047
1048     for filename in glob.glob1(syslinux_target, "*.c32"):
1049         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1050
1051     # copy the addons_*.cfg file to the new syslinux directory
1052     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1053
1054     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1055
1056     # copy all grub files from ISO
1057     glob_and_copy(iso_mount + '/boot/grub/*', grub_target)
1058
1059     # finally (after all GRUB files have been been installed) build static loopback.cfg
1060     build_loopbackcfg(target)
1061
1062
1063 def install_iso_files(grml_flavour, iso_mount, device, target):
1064     """Copy files from ISO to given target
1065
1066     @grml_flavour: name of grml flavour the configuration should be generated for
1067     @iso_mount: path where a grml ISO is mounted on
1068     @device: device/partition where bootloader should be installed to
1069     @target: path where grml's main files should be copied to"""
1070
1071     global GRML_DEFAULT
1072     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1073     if options.dryrun:
1074         return 0
1075     elif not options.bootloaderonly:
1076         logging.info("Copying files. This might take a while....")
1077         try:
1078             copy_system_files(grml_flavour, iso_mount, target)
1079             copy_grml_files(grml_flavour, iso_mount, target)
1080         except CriticalException, error:
1081             logging.critical("Execution failed: %s", error)
1082             sys.exit(1)
1083
1084     if not options.skipaddons:
1085         if not search_file('addons', iso_mount):
1086             logging.info("Could not find addons, therefore not installing.")
1087         else:
1088             copy_addons(iso_mount, target)
1089
1090     if not options.copyonly:
1091         copy_bootloader_files(iso_mount, target, grml_flavour)
1092
1093         if not options.dryrun:
1094             handle_bootloader_config(grml_flavour, device, target)
1095
1096     # make sure we sync filesystems before returning
1097     proc = subprocess.Popen(["sync"])
1098     proc.wait()
1099
1100
1101 def get_flavour(flavour_str):
1102     """Returns the flavour of a grml version string
1103     """
1104     return re.match(r'[\w-]*', flavour_str).group()
1105
1106
1107 def identify_grml_flavour(mountpath):
1108     """Get name of grml flavour
1109
1110     @mountpath: path where the grml ISO is mounted to
1111     @return: name of grml-flavour"""
1112
1113     version_files = search_file('grml-version', mountpath, lst_return=True)
1114
1115     if not version_files:
1116         logging.critical("Error: could not find grml-version file.")
1117         raise
1118
1119     flavours = []
1120     logging.debug("version_files = %s", version_files)
1121     for version_file in version_files:
1122         tmpfile = None
1123         try:
1124             tmpfile = open(version_file, 'r')
1125             for line in tmpfile.readlines():
1126                 flavours.append(get_flavour(line))
1127         except TypeError, e:
1128             raise
1129         except Exception, e:
1130             logging.critical("Unexpected error: %s", e)
1131             raise
1132         finally:
1133             if tmpfile:
1134                 tmpfile.close()
1135
1136     return flavours
1137
1138
1139 def get_bootoptions(grml_flavour):
1140     """Returns bootoptions for specific flavour
1141
1142     @grml_flavour: name of the grml_flavour
1143     """
1144     # do NOT write "None" in kernel cmdline
1145     if not options.bootoptions:
1146         bootopt = ""
1147     else:
1148         bootopt = " ".join(options.bootoptions)
1149     bootopt = bootopt.replace("%flavour", grml_flavour)
1150     return bootopt
1151
1152
1153 def handle_grub_config(grml_flavour, device, target):
1154     """Main handler for generating grub (v1 and v2) configuration
1155
1156     @grml_flavour: name of grml flavour the configuration should be generated for
1157     @device: device/partition where grub should be installed to
1158     @target: path of grub's configuration files"""
1159
1160     global UUID
1161
1162     logging.debug("Updating grub configuration")
1163
1164     grub_target = target + '/boot/grub/'
1165
1166     bootid_re = re.compile("bootid=[\w_-]+")
1167     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1168
1169     bootopt = get_bootoptions(grml_flavour)
1170
1171     remove_regexes = []
1172     option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1173
1174     if options.removeoption:
1175         for regex in options.removeoption:
1176             remove_regexes.append(re.compile(regex))
1177
1178     shortname = get_shortname(grml_flavour)
1179     for filename in glob.glob(grub_target + '*.cfg'):
1180         for line in fileinput.input(filename, inplace=1):
1181             line = line.rstrip("\r\n")
1182             if option_re.search(line):
1183                 line = bootid_re.sub('', line)
1184                 if shortname in filename:
1185                     line = live_media_path_re.sub('', line)
1186                     line = line.rstrip() + ' live-media-path=/live/%s/ ' % (grml_flavour)
1187                 line = line.rstrip() + r' bootid=%s %s ' % (UUID, bootopt)
1188                 for regex in remove_regexes:
1189                     line = regex.sub(' ', line)
1190             print line
1191         fileinput.close()
1192
1193
1194 def initial_syslinux_config(target):
1195     """Generates intial syslinux configuration
1196
1197     @target path of syslinux's configuration files"""
1198
1199     target = target + "/"
1200     filename = target + "grmlmain.cfg"
1201     if os.path.isfile(target + "grmlmain.cfg"):
1202         return
1203     data = open(filename, "w")
1204     data.write(generate_main_syslinux_config())
1205     data.close()
1206
1207     filename = target + "hiddens.cfg"
1208     data = open(filename, "w")
1209     data.write("include hidden.cfg\n")
1210     data.close()
1211
1212
1213 def add_entry_if_not_present(filename, entry):
1214     """Write entry into filename if entry is not already in the file
1215
1216     @filanme: name of the file
1217     @entry: data to write to the file
1218     """
1219     data = open(filename, "a+")
1220     for line in data:
1221         if line == entry:
1222             break
1223     else:
1224         data.write(entry)
1225
1226     data.close()
1227
1228
1229 def get_flavour_filename(flavour):
1230     """Generate a iso9960 save filename out of the specified flavour
1231
1232     @flavour: grml flavour
1233     """
1234     return flavour.replace('-', '_')
1235
1236
1237 def adjust_syslinux_bootoptions(src, flavour):
1238     """Adjust existing bootoptions of specified syslinux config to
1239     grml2usb specific ones, e.g. change the location of the kernel...
1240
1241     @src: config file to alter
1242     @flavour: grml flavour
1243     """
1244
1245     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1246     # flavour_re = re.compile("(label.*)(grml\w+)")
1247     default_re = re.compile("(default.cfg)")
1248     bootid_re = re.compile("bootid=[\w_-]+")
1249     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1250
1251     bootopt = get_bootoptions(flavour)
1252
1253     regexe = []
1254     option_re = None
1255     if options.removeoption:
1256         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1257
1258         for regex in options.removeoption:
1259             regexe.append(re.compile(r'%s' % regex))
1260
1261     for line in fileinput.input(src, inplace=1):
1262         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1263         line = default_re.sub(r'%s-\1' % flavour, line)
1264         line = bootid_re.sub('', line)
1265         line = live_media_path_re.sub('', line)
1266         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1267         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1268         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1269         if option_re and option_re.search(line):
1270             for regex in regexe:
1271                 line = regex.sub(' ', line)
1272         sys.stdout.write(line)
1273     fileinput.close()
1274
1275
1276 def adjust_labels(src, replacement):
1277     """Adjust the specified labels in the syslinux config file src with
1278     specified replacement
1279     """
1280     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1281     for line in fileinput.input(src, inplace=1):
1282         line = label_re.sub(replacement, line)
1283         sys.stdout.write(line)
1284     fileinput.close()
1285
1286
1287 def add_syslinux_entry(filename, grml_flavour):
1288     """Add includes for a specific grml_flavour to the specified filename
1289
1290     @filename: syslinux config file
1291     @grml_flavour: grml flavour to add
1292     """
1293
1294     entry_filename = "option_%s.cfg" % grml_flavour
1295     entry = "include %s\n" % entry_filename
1296
1297     add_entry_if_not_present(filename, entry)
1298     path = os.path.dirname(filename)
1299
1300     data = open(path + "/" + entry_filename, "w")
1301     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1302     data.close()
1303
1304
1305 def modify_filenames(grml_flavour, target, filenames):
1306     """Replace the standard filenames with the new ones
1307
1308     @grml_flavour: grml-flavour strin
1309     @target: directory where the files are located
1310     @filenames: list of filenames to alter
1311     """
1312     grml_filename = get_flavour_filename(grml_flavour)
1313     for filename in filenames:
1314         old_filename = "%s/%s" % (target, filename)
1315         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1316         os.rename(old_filename, new_filename)
1317
1318
1319 def remove_default_entry(filename):
1320     """Remove the default entry from specified syslinux file
1321
1322     @filename: syslinux config file
1323     """
1324     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1325     for line in fileinput.input(filename, inplace=1):
1326         if default_re.match(line):
1327             continue
1328         sys.stdout.write(line)
1329     fileinput.close()
1330
1331
1332 def handle_syslinux_config(grml_flavour, target):
1333     """Main handler for generating syslinux configuration
1334
1335     @grml_flavour: name of grml flavour the configuration should be generated for
1336     @target: path of syslinux's configuration files"""
1337
1338     logging.debug("Generating syslinux configuration")
1339     syslinux_target = target + '/boot/syslinux/'
1340     # should be present via  copy_bootloader_files(), but make sure it exits:
1341     execute(mkdir, syslinux_target)
1342     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1343
1344     # install main configuration only *once*, no matter how many ISOs we have:
1345     syslinux_config_file = open(syslinux_cfg, 'w')
1346     syslinux_config_file.write("TIMEOUT 300\n")
1347     syslinux_config_file.write("include vesamenu.cfg\n")
1348     syslinux_config_file.close()
1349
1350     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1351     prompt_name.write('menu label S^yslinux prompt\n')
1352     prompt_name.close()
1353
1354     initial_syslinux_config(syslinux_target)
1355     flavour_filename = get_flavour_filename(grml_flavour)
1356
1357     if search_file('default.cfg', syslinux_target):
1358         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1359
1360     filename = search_file("new_hidden.cfg", syslinux_target)
1361
1362     # process hidden file
1363     if not search_file("hidden.cfg", syslinux_target):
1364         new_hidden = syslinux_target + "hidden.cfg"
1365         os.rename(filename, new_hidden)
1366         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1367     else:
1368         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1369         os.rename(filename, new_hidden_file)
1370         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1371         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1372         entry = 'include %s_hidden.cfg\n' % flavour_filename
1373         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1374
1375     new_default = "%s_default.cfg" % (flavour_filename)
1376     entry = 'include %s\n' % new_default
1377     defaults_file = '%s/defaults.cfg' % syslinux_target
1378     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1379     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1380
1381     if os.path.isfile(defaults_file):
1382
1383         # remove default menu entry in menu
1384         remove_default_entry(new_default_with_path)
1385
1386         # adjust all labels for additional isos
1387         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1388         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1389
1390     # always adjust bootoptions
1391     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1392     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1393
1394     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1395
1396     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1397
1398
1399 def handle_bootloader_config(grml_flavour, device, target):
1400     """Main handler for generating bootloader's configuration
1401
1402     @grml_flavour: name of grml flavour the configuration should be generated for
1403     @device: device/partition where bootloader should be installed to
1404     @target: path of bootloader's configuration files"""
1405
1406     global UUID
1407     UUID = get_uuid(target)
1408     if options.skipsyslinuxconfig:
1409         logging.info("Skipping generation of syslinux configuration as requested.")
1410     else:
1411         try:
1412             handle_syslinux_config(grml_flavour, target)
1413         except CriticalException, error:
1414             logging.critical("Fatal: %s", error)
1415             sys.exit(1)
1416
1417     if options.skipgrubconfig:
1418         logging.info("Skipping generation of grub configuration as requested.")
1419     else:
1420         try:
1421             handle_grub_config(grml_flavour, device, target)
1422         except CriticalException, error:
1423             logging.critical("Fatal: %s", error)
1424             sys.exit(1)
1425
1426
1427 def install(image, device):
1428     """Install a grml image to the specified device
1429
1430     @image: directory or is file
1431     @device: partition or directory to install the device
1432     """
1433     iso_mountpoint = image
1434     remove_image_mountpoint = False
1435     if os.path.isdir(image):
1436         logging.info("Using %s as install base", image)
1437     else:
1438         logging.info("Using ISO %s", image)
1439         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1440         register_tmpfile(iso_mountpoint)
1441         remove_image_mountpoint = True
1442         try:
1443             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1444         except CriticalException, error:
1445             logging.critical("Fatal: %s", error)
1446             sys.exit(1)
1447
1448     try:
1449         install_grml(iso_mountpoint, device)
1450     finally:
1451         if remove_image_mountpoint:
1452             try:
1453                 remove_mountpoint(iso_mountpoint)
1454             except CriticalException, error:
1455                 logging.critical("Fatal: %s", error)
1456                 cleanup()
1457
1458
1459 def install_grml(mountpoint, device):
1460     """Main logic for copying files of the currently running grml system.
1461
1462     @mountpoin: directory where currently running live system resides (usually /live/image)
1463     @device: partition where the specified ISO should be installed to"""
1464
1465     device_mountpoint = device
1466     if os.path.isdir(device):
1467         logging.info("Specified device is not a directory, therefore not mounting.")
1468         remove_device_mountpoint = False
1469     else:
1470         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1471         register_tmpfile(device_mountpoint)
1472         remove_device_mountpoint = True
1473         try:
1474             check_for_fat(device)
1475             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1476         except CriticalException, error:
1477             try:
1478                 mount(device, device_mountpoint, "")
1479             except CriticalException, error:
1480                 logging.critical("Fatal: %s", error)
1481                 raise
1482     try:
1483         grml_flavours = identify_grml_flavour(mountpoint)
1484         for flavour in set(grml_flavours):
1485             if not flavour:
1486                 logging.warning("No valid flavour found, please check your iso")
1487             logging.info("Identified grml flavour \"%s\".", flavour)
1488             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1489             GRML_FLAVOURS.add(flavour)
1490     finally:
1491         if remove_device_mountpoint:
1492             remove_mountpoint(device_mountpoint)
1493
1494
1495 def remove_mountpoint(mountpoint):
1496     """remove a registred mountpoint
1497     """
1498
1499     try:
1500         unmount(mountpoint, "")
1501         if os.path.isdir(mountpoint):
1502             os.rmdir(mountpoint)
1503             unregister_tmpfile(mountpoint)
1504     except CriticalException, error:
1505         logging.critical("Fatal: %s", error)
1506         cleanup()
1507
1508
1509 def handle_mbr(device):
1510     """Main handler for installing master boot record (MBR)
1511
1512     @device: device where the MBR should be installed to"""
1513
1514     if options.dryrun:
1515         logging.info("Would install MBR")
1516         return 0
1517
1518     if device[-1:].isdigit():
1519         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1520         partition_number = int(device[-1:]) - 1
1521     else:
1522         logging.warn("Could not detect partition number, not activating partition")
1523         partition_number = None
1524
1525     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1526     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1527     if mbr_device == "/dev/loop":
1528         mbr_device = device
1529         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1530
1531     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1532     if options.syslinuxmbr:
1533         mbrcode = '/usr/lib/syslinux/mbr.bin'
1534     elif options.mbrmenu:
1535         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1536
1537     try:
1538         install_mbr(mbrcode, mbr_device, partition_number, True)
1539     except IOError, error:
1540         logging.critical("Execution failed: %s", error)
1541         sys.exit(1)
1542     except Exception, error:
1543         logging.critical("Execution failed: %s", error)
1544         sys.exit(1)
1545
1546
1547 def handle_vfat(device):
1548     """Check for FAT specific settings and options
1549
1550     @device: device that should checked / formated"""
1551
1552     # make sure we have mkfs.vfat available
1553     if options.fat16:
1554         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1555             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1556             logging.critical('Please make sure to install dosfstools.')
1557             sys.exit(1)
1558
1559         if options.force:
1560             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1561         else:
1562             # make sure the user is aware of what he is doing
1563             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1564             if f == "y" or f == "Y":
1565                 logging.info("Note: you can skip this question using the option --force")
1566             else:
1567                 sys.exit(1)
1568         try:
1569             mkfs_fat16(device)
1570         except CriticalException, error:
1571             logging.critical("Execution failed: %s", error)
1572             sys.exit(1)
1573
1574     # check for vfat filesystem
1575     if device is not None and not os.path.isdir(device) and options.syslinux:
1576         try:
1577             check_for_fat(device)
1578         except CriticalException, error:
1579             logging.critical("Execution failed: %s", error)
1580             sys.exit(1)
1581
1582     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1583         print "Warning: the specified device %s does not look like a removable usb device." % device
1584         f = raw_input("Do you really want to continue? y/N ")
1585         if f == "y" or f == "Y":
1586             pass
1587         else:
1588             sys.exit(1)
1589
1590
1591 def handle_compat_warning(device):
1592     """Backwards compatible checks
1593
1594     @device: device that should be checked"""
1595
1596     # make sure we can replace old grml2usb script and warn user when using old way of life:
1597     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1598         print "Warning: the semantics of grml2usb has changed."
1599         print "Instead of using grml2usb /path/to/iso %s you might" % device
1600         print "want to use grml2usb /path/to/iso /dev/... instead."
1601         print "Please check out the grml2usb manpage for details."
1602         f = raw_input("Do you really want to continue? y/N ")
1603         if f == "y" or f == "Y":
1604             pass
1605         else:
1606             sys.exit(1)
1607
1608
1609 def handle_logging():
1610     """Log handling and configuration"""
1611
1612     if options.verbose and options.quiet:
1613         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1614
1615     if options.verbose:
1616         FORMAT = "Debug: %(asctime)-15s %(message)s"
1617         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1618     elif options.quiet:
1619         FORMAT = "Critical: %(message)s"
1620         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1621     else:
1622         FORMAT = "%(message)s"
1623         logging.basicConfig(level=logging.INFO, format=FORMAT)
1624
1625
1626 def handle_bootloader(device):
1627     """wrapper for installing bootloader
1628
1629     @device: device where bootloader should be installed to"""
1630
1631     # Install bootloader only if not using the --copy-only option
1632     if options.copyonly:
1633         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1634     elif os.path.isdir(device):
1635         logging.info("Not installing bootloader as %s is a directory.", device)
1636     else:
1637         install_bootloader(device)
1638
1639
1640 def check_options(opts):
1641     """Check compability of provided user opts
1642
1643     @opts option dict from OptionParser
1644     """
1645     if opts.grubmbr and not opts.grub:
1646         logging.critical("Error: --grub-mbr requires --grub option.")
1647         sys.exit(1)
1648
1649
1650 def check_programs():
1651     """check if all needed programs are installed"""
1652     if options.grub:
1653         if not which("grub-install"):
1654             logging.critical("Fatal: grub-install not available (please install the "
1655                              + "grub package or drop the --grub option)")
1656             sys.exit(1)
1657
1658     if options.syslinux:
1659         if not which("syslinux"):
1660             logging.critical("Fatal: syslinux not available (please install the "
1661                              + "syslinux package or use the --grub option)")
1662             sys.exit(1)
1663
1664     if not which("rsync"):
1665         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1666         sys.exit(1)
1667
1668
1669 def load_loop():
1670     """Runs modprobe loop and throws away it's output"""
1671     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1672     proc.wait()
1673
1674
1675 def main():
1676     """Main function [make pylint happy :)]"""
1677
1678     if options.version:
1679         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1680         sys.exit(0)
1681
1682     if len(args) < 2:
1683         parser.error("invalid usage")
1684
1685     # log handling
1686     handle_logging()
1687
1688     # make sure we have the appropriate permissions
1689     check_uid_root()
1690
1691     check_options(options)
1692
1693     load_loop()
1694
1695     logging.info("Executing grml2usb version %s", PROG_VERSION)
1696
1697     if options.dryrun:
1698         logging.info("Running in simulation mode as requested via option dry-run.")
1699
1700     check_programs()
1701
1702     # specified arguments
1703     device = os.path.realpath(args[len(args) - 1])
1704     isos = args[0:len(args) - 1]
1705
1706     if not os.path.isdir(device):
1707         if device[-1:].isdigit():
1708             if int(device[-1:]) > 4 or device[-2:].isdigit():
1709                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1710                 sys.exit(1)
1711
1712     # provide upgrade path
1713     handle_compat_warning(device)
1714
1715     # check for vfat partition
1716     handle_vfat(device)
1717
1718     # main operation (like installing files)
1719     for iso in isos:
1720         install(iso, device)
1721
1722     # install mbr
1723     is_superfloppy = not device[-1:].isdigit()
1724     if is_superfloppy:
1725         logging.info("Detected superfloppy format - not installing MBR")
1726
1727     if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1728         handle_mbr(device)
1729
1730     handle_bootloader(device)
1731
1732     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1733
1734     for flavour in GRML_FLAVOURS:
1735         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1736
1737     # finally be politely :)
1738     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1739
1740
1741 if __name__ == "__main__":
1742     try:
1743         main()
1744     except KeyboardInterrupt:
1745         logging.info("Received KeyboardInterrupt")
1746         cleanup()
1747
1748 ## END OF FILE #################################################################
1749 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8