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