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