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