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