Fix error message.
[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         logging.critical("Error: could not find grml-version file.")
1112         cleanup()
1113         sys.exit(1)
1114
1115     flavours = []
1116     logging.debug("version_files = %s", version_files)
1117     for version_file in version_files:
1118         tmpfile = None
1119         try:
1120             tmpfile = open(version_file, 'r')
1121             for line in tmpfile.readlines():
1122                 flavours.append(get_flavour(line))
1123         except TypeError, e:
1124             raise
1125         except Exception, e:
1126             logging.critical("Unexpected error: %s", e)
1127             raise
1128         finally:
1129             if tmpfile:
1130                 tmpfile.close()
1131
1132     return flavours
1133
1134
1135 def get_bootoptions(grml_flavour):
1136     """Returns bootoptions for specific flavour
1137
1138     @grml_flavour: name of the grml_flavour
1139     """
1140     # do NOT write "None" in kernel cmdline
1141     if not options.bootoptions:
1142         bootopt = ""
1143     else:
1144         bootopt = " ".join(options.bootoptions)
1145     bootopt = bootopt.replace("%flavour", grml_flavour)
1146     return bootopt
1147
1148
1149 def handle_grub_config(grml_flavour, device, target):
1150     """Main handler for generating grub (v1 and v2) configuration
1151
1152     @grml_flavour: name of grml flavour the configuration should be generated for
1153     @device: device/partition where grub should be installed to
1154     @target: path of grub's configuration files"""
1155
1156     global UUID
1157
1158     logging.debug("Updating grub configuration")
1159
1160     grub_target = target + '/boot/grub/'
1161
1162     bootid_re = re.compile("bootid=[\w_-]+")
1163     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1164
1165     bootopt = get_bootoptions(grml_flavour)
1166
1167     remove_regexes = []
1168     option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1169
1170     if options.removeoption:
1171         for regex in options.removeoption:
1172             remove_regexes.append(re.compile(regex))
1173
1174     shortname = get_shortname(grml_flavour)
1175     for filename in glob.glob(grub_target + '*.cfg'):
1176         for line in fileinput.input(filename, inplace=1):
1177             line = line.rstrip("\r\n")
1178             if option_re.search(line):
1179                 line = bootid_re.sub('', line)
1180                 if shortname in filename:
1181                     line = live_media_path_re.sub('', line)
1182                     line = line.rstrip() + ' live-media-path=/live/%s/ ' % (grml_flavour)
1183                 line = line.rstrip() + r' bootid=%s %s ' % (UUID, bootopt)
1184                 for regex in remove_regexes:
1185                     line = regex.sub(' ', line)
1186             print line
1187         fileinput.close()
1188
1189
1190 def initial_syslinux_config(target):
1191     """Generates intial syslinux configuration
1192
1193     @target path of syslinux's configuration files"""
1194
1195     target = target + "/"
1196     filename = target + "grmlmain.cfg"
1197     if os.path.isfile(target + "grmlmain.cfg"):
1198         return
1199     data = open(filename, "w")
1200     data.write(generate_main_syslinux_config())
1201     data.close()
1202
1203     filename = target + "hiddens.cfg"
1204     data = open(filename, "w")
1205     data.write("include hidden.cfg\n")
1206     data.close()
1207
1208
1209 def add_entry_if_not_present(filename, entry):
1210     """Write entry into filename if entry is not already in the file
1211
1212     @filanme: name of the file
1213     @entry: data to write to the file
1214     """
1215     data = open(filename, "a+")
1216     for line in data:
1217         if line == entry:
1218             break
1219     else:
1220         data.write(entry)
1221
1222     data.close()
1223
1224
1225 def get_flavour_filename(flavour):
1226     """Generate a iso9960 save filename out of the specified flavour
1227
1228     @flavour: grml flavour
1229     """
1230     return flavour.replace('-', '_')
1231
1232
1233 def adjust_syslinux_bootoptions(src, flavour):
1234     """Adjust existing bootoptions of specified syslinux config to
1235     grml2usb specific ones, e.g. change the location of the kernel...
1236
1237     @src: config file to alter
1238     @flavour: grml flavour
1239     """
1240
1241     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1242     # flavour_re = re.compile("(label.*)(grml\w+)")
1243     default_re = re.compile("(default.cfg)")
1244     bootid_re = re.compile("bootid=[\w_-]+")
1245     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1246
1247     bootopt = get_bootoptions(flavour)
1248
1249     regexe = []
1250     option_re = None
1251     if options.removeoption:
1252         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1253
1254         for regex in options.removeoption:
1255             regexe.append(re.compile(r'%s' % regex))
1256
1257     for line in fileinput.input(src, inplace=1):
1258         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1259         line = default_re.sub(r'%s-\1' % flavour, line)
1260         line = bootid_re.sub('', line)
1261         line = live_media_path_re.sub('', line)
1262         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1263         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1264         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1265         if option_re and option_re.search(line):
1266             for regex in regexe:
1267                 line = regex.sub(' ', line)
1268         sys.stdout.write(line)
1269     fileinput.close()
1270
1271
1272 def adjust_labels(src, replacement):
1273     """Adjust the specified labels in the syslinux config file src with
1274     specified replacement
1275     """
1276     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1277     for line in fileinput.input(src, inplace=1):
1278         line = label_re.sub(replacement, line)
1279         sys.stdout.write(line)
1280     fileinput.close()
1281
1282
1283 def add_syslinux_entry(filename, grml_flavour):
1284     """Add includes for a specific grml_flavour to the specified filename
1285
1286     @filename: syslinux config file
1287     @grml_flavour: grml flavour to add
1288     """
1289
1290     entry_filename = "option_%s.cfg" % grml_flavour
1291     entry = "include %s\n" % entry_filename
1292
1293     add_entry_if_not_present(filename, entry)
1294     path = os.path.dirname(filename)
1295
1296     data = open(path + "/" + entry_filename, "w")
1297     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1298     data.close()
1299
1300
1301 def modify_filenames(grml_flavour, target, filenames):
1302     """Replace the standard filenames with the new ones
1303
1304     @grml_flavour: grml-flavour strin
1305     @target: directory where the files are located
1306     @filenames: list of filenames to alter
1307     """
1308     grml_filename = get_flavour_filename(grml_flavour)
1309     for filename in filenames:
1310         old_filename = "%s/%s" % (target, filename)
1311         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1312         os.rename(old_filename, new_filename)
1313
1314
1315 def remove_default_entry(filename):
1316     """Remove the default entry from specified syslinux file
1317
1318     @filename: syslinux config file
1319     """
1320     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1321     for line in fileinput.input(filename, inplace=1):
1322         if default_re.match(line):
1323             continue
1324         sys.stdout.write(line)
1325     fileinput.close()
1326
1327
1328 def handle_syslinux_config(grml_flavour, target):
1329     """Main handler for generating syslinux configuration
1330
1331     @grml_flavour: name of grml flavour the configuration should be generated for
1332     @target: path of syslinux's configuration files"""
1333
1334     logging.debug("Generating syslinux configuration")
1335     syslinux_target = target + '/boot/syslinux/'
1336     # should be present via  copy_bootloader_files(), but make sure it exits:
1337     execute(mkdir, syslinux_target)
1338     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1339
1340     # install main configuration only *once*, no matter how many ISOs we have:
1341     syslinux_config_file = open(syslinux_cfg, 'w')
1342     syslinux_config_file.write("TIMEOUT 300\n")
1343     syslinux_config_file.write("include vesamenu.cfg\n")
1344     syslinux_config_file.close()
1345
1346     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1347     prompt_name.write('menu label S^yslinux prompt\n')
1348     prompt_name.close()
1349
1350     initial_syslinux_config(syslinux_target)
1351     flavour_filename = get_flavour_filename(grml_flavour)
1352
1353     if search_file('default.cfg', syslinux_target):
1354         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1355
1356     filename = search_file("new_hidden.cfg", syslinux_target)
1357
1358     # process hidden file
1359     if not search_file("hidden.cfg", syslinux_target):
1360         new_hidden = syslinux_target + "hidden.cfg"
1361         os.rename(filename, new_hidden)
1362         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1363     else:
1364         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1365         os.rename(filename, new_hidden_file)
1366         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1367         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1368         entry = 'include %s_hidden.cfg\n' % flavour_filename
1369         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1370
1371     new_default = "%s_default.cfg" % (flavour_filename)
1372     entry = 'include %s\n' % new_default
1373     defaults_file = '%s/defaults.cfg' % syslinux_target
1374     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1375     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1376
1377     if os.path.isfile(defaults_file):
1378
1379         # remove default menu entry in menu
1380         remove_default_entry(new_default_with_path)
1381
1382         # adjust all labels for additional isos
1383         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1384         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1385
1386     # always adjust bootoptions
1387     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1388     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1389
1390     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1391
1392     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1393
1394
1395 def handle_bootloader_config(grml_flavour, device, target):
1396     """Main handler for generating bootloader's configuration
1397
1398     @grml_flavour: name of grml flavour the configuration should be generated for
1399     @device: device/partition where bootloader should be installed to
1400     @target: path of bootloader's configuration files"""
1401
1402     global UUID
1403     UUID = get_uuid(target)
1404     if options.skipsyslinuxconfig:
1405         logging.info("Skipping generation of syslinux configuration as requested.")
1406     else:
1407         try:
1408             handle_syslinux_config(grml_flavour, target)
1409         except CriticalException, error:
1410             logging.critical("Fatal: %s", error)
1411             sys.exit(1)
1412
1413     if options.skipgrubconfig:
1414         logging.info("Skipping generation of grub configuration as requested.")
1415     else:
1416         try:
1417             handle_grub_config(grml_flavour, device, target)
1418         except CriticalException, error:
1419             logging.critical("Fatal: %s", error)
1420             sys.exit(1)
1421
1422
1423 def install(image, device):
1424     """Install a grml image to the specified device
1425
1426     @image: directory or is file
1427     @device: partition or directory to install the device
1428     """
1429     iso_mountpoint = image
1430     remove_image_mountpoint = False
1431     if os.path.isdir(image):
1432         logging.info("Using %s as install base", image)
1433     else:
1434         logging.info("Using ISO %s", image)
1435         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=options.tmpdir)
1436         register_tmpfile(iso_mountpoint)
1437         remove_image_mountpoint = True
1438         try:
1439             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1440         except CriticalException, error:
1441             logging.critical("Fatal: %s", error)
1442             sys.exit(1)
1443
1444     try:
1445         install_grml(iso_mountpoint, device)
1446     finally:
1447         if remove_image_mountpoint:
1448             try:
1449                 remove_mountpoint(iso_mountpoint)
1450             except CriticalException, error:
1451                 logging.critical("Fatal: %s", error)
1452                 cleanup()
1453
1454
1455 def install_grml(mountpoint, device):
1456     """Main logic for copying files of the currently running grml system.
1457
1458     @mountpoin: directory where currently running live system resides (usually /live/image)
1459     @device: partition where the specified ISO should be installed to"""
1460
1461     device_mountpoint = device
1462     if os.path.isdir(device):
1463         logging.info("Specified device is a directory, therefore not mounting.")
1464         remove_device_mountpoint = False
1465     else:
1466         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1467         register_tmpfile(device_mountpoint)
1468         remove_device_mountpoint = True
1469         try:
1470             check_for_fat(device)
1471             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1472         except CriticalException, error:
1473             try:
1474                 mount(device, device_mountpoint, "")
1475             except CriticalException, error:
1476                 logging.critical("Fatal: %s", error)
1477                 raise
1478     try:
1479         grml_flavours = identify_grml_flavour(mountpoint)
1480         for flavour in set(grml_flavours):
1481             if not flavour:
1482                 logging.warning("No valid flavour found, please check your iso")
1483             logging.info("Identified grml flavour \"%s\".", flavour)
1484             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1485             GRML_FLAVOURS.add(flavour)
1486     finally:
1487         if remove_device_mountpoint:
1488             remove_mountpoint(device_mountpoint)
1489
1490
1491 def remove_mountpoint(mountpoint):
1492     """remove a registred mountpoint
1493     """
1494
1495     try:
1496         unmount(mountpoint, "")
1497         if os.path.isdir(mountpoint):
1498             os.rmdir(mountpoint)
1499             unregister_tmpfile(mountpoint)
1500     except CriticalException, error:
1501         logging.critical("Fatal: %s", error)
1502         cleanup()
1503
1504
1505 def handle_mbr(device):
1506     """Main handler for installing master boot record (MBR)
1507
1508     @device: device where the MBR should be installed to"""
1509
1510     if options.dryrun:
1511         logging.info("Would install MBR")
1512         return 0
1513
1514     if device[-1:].isdigit():
1515         mbr_device = re.match(r'(.*?)\d*$', device).group(1)
1516         partition_number = int(device[-1:]) - 1
1517     else:
1518         logging.warn("Could not detect partition number, not activating partition")
1519         partition_number = None
1520
1521     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1522     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1523     if mbr_device == "/dev/loop":
1524         mbr_device = device
1525         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1526
1527     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1528     if options.syslinuxmbr:
1529         mbrcode = '/usr/lib/syslinux/mbr.bin'
1530     elif options.mbrmenu:
1531         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1532
1533     try:
1534         install_mbr(mbrcode, mbr_device, partition_number, True)
1535     except IOError, error:
1536         logging.critical("Execution failed: %s", error)
1537         sys.exit(1)
1538     except Exception, error:
1539         logging.critical("Execution failed: %s", error)
1540         sys.exit(1)
1541
1542
1543 def handle_vfat(device):
1544     """Check for FAT specific settings and options
1545
1546     @device: device that should checked / formated"""
1547
1548     # make sure we have mkfs.vfat available
1549     if options.fat16:
1550         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1551             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1552             logging.critical('Please make sure to install dosfstools.')
1553             sys.exit(1)
1554
1555         if options.force:
1556             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1557         else:
1558             # make sure the user is aware of what he is doing
1559             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1560             if f == "y" or f == "Y":
1561                 logging.info("Note: you can skip this question using the option --force")
1562             else:
1563                 sys.exit(1)
1564         try:
1565             mkfs_fat16(device)
1566         except CriticalException, error:
1567             logging.critical("Execution failed: %s", error)
1568             sys.exit(1)
1569
1570     # check for vfat filesystem
1571     if device is not None and not os.path.isdir(device) and options.syslinux:
1572         try:
1573             check_for_fat(device)
1574         except CriticalException, error:
1575             logging.critical("Execution failed: %s", error)
1576             sys.exit(1)
1577
1578     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1579         print "Warning: the specified device %s does not look like a removable usb device." % device
1580         f = raw_input("Do you really want to continue? y/N ")
1581         if f == "y" or f == "Y":
1582             pass
1583         else:
1584             sys.exit(1)
1585
1586
1587 def handle_compat_warning(device):
1588     """Backwards compatible checks
1589
1590     @device: device that should be checked"""
1591
1592     # make sure we can replace old grml2usb script and warn user when using old way of life:
1593     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1594         print "Warning: the semantics of grml2usb has changed."
1595         print "Instead of using grml2usb /path/to/iso %s you might" % device
1596         print "want to use grml2usb /path/to/iso /dev/... instead."
1597         print "Please check out the grml2usb manpage for details."
1598         f = raw_input("Do you really want to continue? y/N ")
1599         if f == "y" or f == "Y":
1600             pass
1601         else:
1602             sys.exit(1)
1603
1604
1605 def handle_logging():
1606     """Log handling and configuration"""
1607
1608     if options.verbose and options.quiet:
1609         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1610
1611     if options.verbose:
1612         FORMAT = "Debug: %(asctime)-15s %(message)s"
1613         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1614     elif options.quiet:
1615         FORMAT = "Critical: %(message)s"
1616         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1617     else:
1618         FORMAT = "%(message)s"
1619         logging.basicConfig(level=logging.INFO, format=FORMAT)
1620
1621
1622 def handle_bootloader(device):
1623     """wrapper for installing bootloader
1624
1625     @device: device where bootloader should be installed to"""
1626
1627     # Install bootloader only if not using the --copy-only option
1628     if options.copyonly:
1629         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1630     elif os.path.isdir(device):
1631         logging.info("Not installing bootloader as %s is a directory.", device)
1632     else:
1633         install_bootloader(device)
1634
1635
1636 def check_options(opts):
1637     """Check compability of provided user opts
1638
1639     @opts option dict from OptionParser
1640     """
1641     if opts.grubmbr and not opts.grub:
1642         logging.critical("Error: --grub-mbr requires --grub option.")
1643         sys.exit(1)
1644
1645
1646 def check_programs():
1647     """check if all needed programs are installed"""
1648     if options.grub:
1649         if not which("grub-install"):
1650             logging.critical("Fatal: grub-install not available (please install the "
1651                              + "grub package or drop the --grub option)")
1652             sys.exit(1)
1653
1654     if options.syslinux:
1655         if not which("syslinux"):
1656             logging.critical("Fatal: syslinux not available (please install the "
1657                              + "syslinux package or use the --grub option)")
1658             sys.exit(1)
1659
1660     if not which("rsync"):
1661         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1662         sys.exit(1)
1663
1664
1665 def load_loop():
1666     """Runs modprobe loop and throws away it's output"""
1667     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1668     proc.wait()
1669
1670
1671 def main():
1672     """Main function [make pylint happy :)]"""
1673
1674     if options.version:
1675         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1676         sys.exit(0)
1677
1678     if len(args) < 2:
1679         parser.error("invalid usage")
1680
1681     # log handling
1682     handle_logging()
1683
1684     # make sure we have the appropriate permissions
1685     check_uid_root()
1686
1687     check_options(options)
1688
1689     load_loop()
1690
1691     logging.info("Executing grml2usb version %s", PROG_VERSION)
1692
1693     if options.dryrun:
1694         logging.info("Running in simulation mode as requested via option dry-run.")
1695
1696     check_programs()
1697
1698     # specified arguments
1699     device = os.path.realpath(args[len(args) - 1])
1700     isos = args[0:len(args) - 1]
1701
1702     if not os.path.isdir(device):
1703         if device[-1:].isdigit():
1704             if int(device[-1:]) > 4 or device[-2:].isdigit():
1705                 logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1706                 sys.exit(1)
1707
1708     # provide upgrade path
1709     handle_compat_warning(device)
1710
1711     # check for vfat partition
1712     handle_vfat(device)
1713
1714     # main operation (like installing files)
1715     for iso in isos:
1716         install(iso, device)
1717
1718     # install mbr
1719     is_superfloppy = not device[-1:].isdigit()
1720     if is_superfloppy:
1721         logging.info("Detected superfloppy format - not installing MBR")
1722
1723     if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1724         handle_mbr(device)
1725
1726     handle_bootloader(device)
1727
1728     logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1729
1730     for flavour in GRML_FLAVOURS:
1731         logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1732
1733     # finally be politely :)
1734     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1735
1736
1737 if __name__ == "__main__":
1738     try:
1739         main()
1740     except KeyboardInterrupt:
1741         logging.info("Received KeyboardInterrupt")
1742         cleanup()
1743
1744 ## END OF FILE #################################################################
1745 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8