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