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