Use "boot flag" instead of "bootflag" also in docs + exceptions
[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     boot_dev, x = get_device_from_partition(device)
507
508     logging.info("Checking for boot flag")
509     try:
510         import parted
511
512         part = get_partition_for_path(device)
513         if part is None:
514             raise HodorException("parted could not find partition")
515         if part.getFlag(parted.PARTITION_BOOT):
516             logging.debug("boot flag is enabled on %s" % device)
517             return
518         else:
519             logging.debug("boot flag is NOT enabled on %s" % device)
520             raise VerifyException(
521                 "Device %s does not have the boot flag set. "
522                 "Please enable it to be able to boot." % device
523             )
524     except ImportError:
525         raise VerifyException(
526             "Could not import parted to verify boot flag on %s, please make sure python3-parted is installed."
527             % device
528         )
529
530
531 def mkfs_fat16(device):
532     """Format specified device with VFAT/FAT16 filesystem.
533
534     @device: partition that should be formated"""
535
536     if options.dryrun:
537         logging.info("Would execute mkfs.vfat -F 16 %s now.", device)
538         return 0
539
540     logging.info("Formating partition with fat16 filesystem")
541     logging.debug("mkfs.vfat -F 16 %s", device)
542     proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
543     proc.wait()
544     if proc.returncode != 0:
545         raise CriticalException("error executing mkfs.vfat")
546
547
548 def generate_isolinux_splash(grml_flavour):
549     """Generate bootsplash for isolinux/syslinux
550
551     @grml_flavour: name of grml flavour the configuration should be generated for"""
552
553     grml_name = grml_flavour
554
555     return """\
556 \ f17\f\18/boot/syslinux/logo.16
557
558 Some information and boot options available via keys F2 - F10. http://grml.org/
559 %(grml_name)s
560 """ % {
561         "grml_name": grml_name
562     }
563
564
565 def generate_main_syslinux_config(*arg):
566     """Generate main configuration for use in syslinux.cfg
567
568     @*arg: just for backward compatibility"""
569     # pylint: disable-msg=W0613
570     # remove warning about unused arg
571
572     return """\
573 label -
574 menu label Default boot modes:
575 menu disable
576 include defaults.cfg
577
578 menu end
579 menu separator
580
581 # flavours:
582 label -
583 menu label Additional boot entries for:
584 menu disable
585 include additional.cfg
586
587 menu separator
588 include options.cfg
589 include addons.cfg
590
591 label help
592   include promptname.cfg
593   config prompt.cfg
594   text help
595                                         Jump to old style isolinux prompt
596                                         featuring further information
597                                         regarding available boot options.
598   endtext
599
600
601 include hiddens.cfg
602 """
603
604
605 def generate_flavour_specific_syslinux_config(grml_flavour):
606     """Generate flavour specific configuration for use in syslinux.cfg
607
608     @grml_flavour: name of grml flavour the configuration should be generated for"""
609
610     return """\
611 menu begin grml %(grml_flavour)s
612     menu title %(display_name)s
613     label mainmenu
614     menu label ^Back to main menu...
615     menu exit
616     menu separator
617     # include config for boot parameters from disk
618     include %(grml_flavour)s_grml.cfg
619     menu hide
620 menu end
621 """ % {
622         "grml_flavour": grml_flavour,
623         "display_name": get_flavour_filename(grml_flavour),
624     }
625
626
627 def install_grub(device):
628     """Install grub on specified device.
629
630     @mntpoint: mountpoint of device where grub should install its files to
631     @device: partition where grub should be installed to"""
632
633     if options.dryrun:
634         logging.info(
635             "Would execute grub-install [--root-directory=mount_point] %s now.", device
636         )
637     else:
638         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
639         register_tmpfile(device_mountpoint)
640         try:
641             # If using --grub-mbr then make sure we install grub in MBR instead of PBR
642             if options.grubmbr:
643                 logging.debug("Using option --grub-mbr ...")
644                 grub_device, x = get_device_from_partition(device)
645             else:
646                 grub_device = device
647
648             set_rw(device)
649             mount(device, device_mountpoint, "")
650
651             logging.info("Installing grub as bootloader")
652             for opt in ["--", "--force"]:
653                 set_rw(device)
654                 set_rw(grub_device)
655                 logging.debug(
656                     "%s --recheck --no-floppy --target=i386-pc --root-directory=%s %s %s",
657                     GRUB_INSTALL,
658                     device_mountpoint,
659                     opt,
660                     grub_device,
661                 )
662                 proc = subprocess.Popen(
663                     [
664                         GRUB_INSTALL,
665                         "--recheck",
666                         "--no-floppy",
667                         "--target=i386-pc",
668                         "--root-directory=%s" % device_mountpoint,
669                         opt,
670                         grub_device,
671                     ],
672                     stdout=open(os.devnull, "r+"),
673                 )
674                 proc.wait()
675                 if proc.returncode == 0:
676                     break
677
678             if proc.returncode != 0:
679                 # raise Exception("error executing grub-install")
680                 logging.critical(
681                     "Fatal: error executing grub-install "
682                     "(please check the grml2usb FAQ or drop the --grub option)"
683                 )
684                 logging.critical(
685                     "Note:  if using grub2 consider using "
686                     "the --grub-mbr option as grub considers PBR problematic."
687                 )
688                 cleanup()
689                 sys.exit(1)
690         except CriticalException as error:
691             logging.critical("Fatal: %s", error)
692             cleanup()
693             sys.exit(1)
694         finally:
695             unmount(device_mountpoint, "")
696             os.rmdir(device_mountpoint)
697             unregister_tmpfile(device_mountpoint)
698
699
700 def install_syslinux(device):
701     """Install syslinux on specified device.
702
703     @device: partition where syslinux should be installed to"""
704
705     if options.dryrun:
706         logging.info("Would install syslinux as bootloader on %s", device)
707         return 0
708
709     set_rw(device)
710
711     # syslinux -d boot/isolinux /dev/sdb1
712     logging.info("Installing syslinux as bootloader")
713     logging.debug("syslinux -d boot/syslinux %s", device)
714     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
715     proc.wait()
716     if proc.returncode != 0:
717         raise CriticalException(
718             "Error executing syslinux (either try --fat16 or use grub?)"
719         )
720
721
722 def install_bootloader(device):
723     """Install bootloader on specified device.
724
725     @device: partition where bootloader should be installed to"""
726
727     # by default we use grub, so install syslinux only on request
728     if options.grub:
729         try:
730             install_grub(device)
731         except CriticalException as error:
732             logging.critical("Fatal: %s", error)
733             cleanup()
734             sys.exit(1)
735     else:
736         try:
737             install_syslinux(device)
738         except CriticalException as error:
739             logging.critical("Fatal: %s", error)
740             cleanup()
741             sys.exit(1)
742
743
744 def install_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
745     """install 'mbr' master boot record (MBR) on a device
746
747     Retrieve the partition table from "device", install an MBR from the
748     "mbrtemplate" file, set the "partition" (0..3) active, and install the
749     result back to "device".
750
751     @mbrtemplate: default MBR file
752
753     @device: name of a file assumed to be a hard disc (or USB stick) image, or
754     something like "/dev/sdb"
755
756     @partition: must be a number between 0 and 3, inclusive
757
758     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
759     ismirbsdmbr) bytes.
760
761     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
762     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
763     false then leave the mirbsdmbr specific flag set to FFh, set all
764     active flags to 0 and set the active flag of the partition to 80h.  Note:
765     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
766     the active flag."""
767
768     logging.info("Installing default MBR")
769
770     if not os.path.isfile(mbrtemplate):
771         logging.error(
772             "Error installing MBR (either try --syslinux-mbr or "
773             'install missing file "%s"?)',
774             mbrtemplate,
775         )
776         raise CriticalException("%s can not be read." % mbrtemplate)
777
778     if partition is not None and ((partition < 0) or (partition > 3)):
779         logging.warn("Cannot activate partition %d", partition)
780         partition = None
781
782     if ismirbsdmbr:
783         nmbrbytes = 439
784     else:
785         nmbrbytes = 440
786
787     tmpf = tempfile.NamedTemporaryFile()
788
789     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
790     proc = subprocess.Popen(
791         ["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
792         stderr=open(os.devnull, "r+"),
793     )
794     proc.wait()
795     if proc.returncode != 0:
796         raise Exception("error executing dd (first run)")
797
798     logging.debug(
799         "executing: dd if=%s of=%s bs=%s count=1 conv=notrunc",
800         mbrtemplate,
801         tmpf.name,
802         nmbrbytes,
803     )
804     proc = subprocess.Popen(
805         [
806             "dd",
807             "if=%s" % mbrtemplate,
808             "of=%s" % tmpf.name,
809             "bs=%s" % nmbrbytes,
810             "count=1",
811             "conv=notrunc",
812         ],
813         stderr=open(os.devnull, "r+"),
814     )
815     proc.wait()
816     if proc.returncode != 0:
817         raise Exception("error executing dd (second run)")
818
819     mbrcode = tmpf.file.read(512)
820     if len(mbrcode) < 512:
821         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
822
823     if partition is not None:
824         if ismirbsdmbr:
825             mbrcode = (
826                 mbrcode[0:439]
827                 + chr(partition).encode("latin-1")
828                 + mbrcode[440:510]
829                 + b"\x55\xAA"
830             )
831         else:
832             actives = [b"\x00", b"\x00", b"\x00", b"\x00"]
833             actives[partition] = b"\x80"
834             mbrcode = (
835                 mbrcode[0:446]
836                 + actives[0]
837                 + mbrcode[447:462]
838                 + actives[1]
839                 + mbrcode[463:478]
840                 + actives[2]
841                 + mbrcode[479:494]
842                 + actives[3]
843                 + mbrcode[495:510]
844                 + b"\x55\xAA"
845             )
846
847     tmpf.file.seek(0)
848     tmpf.file.truncate()
849     tmpf.file.write(mbrcode)
850     tmpf.file.close()
851
852     set_rw(device)
853
854     logging.debug(
855         "executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device
856     )
857     proc = subprocess.Popen(
858         [
859             "dd",
860             "if=%s" % tmpf.name,
861             "of=%s" % device,
862             "bs=512",
863             "count=1",
864             "conv=notrunc",
865         ],
866         stderr=open(os.devnull, "r+"),
867     )
868     proc.wait()
869     if proc.returncode != 0:
870         raise Exception("error executing dd (third run)")
871     del tmpf
872
873     # make sure we sync filesystems before returning
874     logging.debug("executing: sync")
875     proc = subprocess.Popen(["sync"])
876     proc.wait()
877
878     set_rw(device)
879
880
881 def is_writeable(device):
882     """Check if the device is writeable for the current user
883
884     @device: partition where bootloader should be installed to"""
885
886     if not device:
887         return False
888
889     if not os.path.exists(device):
890         return False
891
892     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
893
894
895 def mount(source, target, mount_options):
896     """Mount specified source on given target
897
898     @source: name of device/ISO that should be mounted
899     @target: directory where the ISO should be mounted to
900     @options: mount specific options"""
901
902     # note: options.dryrun does not work here, as we have to
903     # locate files and identify the grml flavour
904
905     for x in open("/proc/mounts", "r").readlines():
906         if x.startswith(source):
907             raise CriticalException(
908                 (
909                     "Error executing mount: %s already mounted - "
910                     "please unmount before invoking grml2usb"
911                 ).format(source)
912             )
913
914     if os.path.isdir(source):
915         logging.debug("Source %s is not a device, therefore not mounting.", source)
916         return 0
917
918     logging.debug("mount %s %s %s", mount_options, source, target)
919     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
920     proc.wait()
921     if proc.returncode != 0:
922         raise CriticalException(
923             "Error executing mount (no filesystem on the partition?)"
924         )
925     else:
926         logging.debug("register_mountpoint(%s)", target)
927         register_mountpoint(target)
928
929
930 def unmount(target, unmount_options):
931     """Unmount specified target
932
933     @target: target where something is mounted on and which should be unmounted
934     @options: options for umount command"""
935
936     # make sure we unmount only already mounted targets
937     target_unmount = False
938     mounts = open("/proc/mounts").readlines()
939     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
940     for line in mounts:
941         if re.match(mountstring, line):
942             target_unmount = True
943
944     if not target_unmount:
945         logging.debug("%s not mounted anymore", target)
946     else:
947         logging.debug("umount %s %s", list(unmount_options), target)
948         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
949         proc.wait()
950         if proc.returncode != 0:
951             raise Exception("Error executing umount")
952         else:
953             logging.debug("unregister_mountpoint(%s)", target)
954             unregister_mountpoint(target)
955
956
957 def check_for_usbdevice(device):
958     """Check whether the specified device is a removable USB device
959
960     @device: device name, like /dev/sda1 or /dev/sda
961     """
962
963     usbdevice = re.match(r"/dev/(.*?)\d*$", device).group(1)
964     # newer systems:
965     usbdev = os.path.realpath("/sys/class/block/" + usbdevice + "/removable")
966     if not os.path.isfile(usbdev):
967         # Ubuntu with kernel 2.6.24 for example:
968         usbdev = os.path.realpath("/sys/block/" + usbdevice + "/removable")
969
970     if os.path.isfile(usbdev):
971         is_usb = open(usbdev).readline()
972         if is_usb.find("1"):
973             return 0
974
975     return 1
976
977
978 def check_for_fat(partition):
979     """Check whether specified partition is a valid VFAT/FAT16 filesystem
980
981     @partition: device name of partition"""
982
983     if not os.access(partition, os.R_OK):
984         raise CriticalException(
985             "Failed to read device %s"
986             " (wrong UID/permissions or device/directory not present?)" % partition
987         )
988
989     try:
990         filesystem = (
991             subprocess.check_output(
992                 ["/sbin/blkid", "-s", "TYPE", "-o", "value", partition]
993             )
994             .decode()
995             .rstrip()
996         )
997
998         if filesystem != "vfat":
999             raise CriticalException(
1000                 "Partition %s does not contain a FAT16 filesystem. "
1001                 "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition)
1002             )
1003
1004     except OSError:
1005         raise CriticalException(
1006             "Sorry, /sbin/blkid not available (install util-linux?)"
1007         )
1008
1009
1010 def mkdir(directory):
1011     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
1012
1013     # just silently pass as it's just fine it the directory exists
1014     if not os.path.isdir(directory):
1015         try:
1016             os.makedirs(directory)
1017         # pylint: disable-msg=W0704
1018         except OSError:
1019             pass
1020
1021
1022 def exec_rsync(source, target):
1023     """Simple wrapper around rsync to install files
1024
1025     @source: source file/directory
1026     @target: target file/directory"""
1027     logging.debug("Source: %s / Target: %s", source, target)
1028     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
1029     proc.wait()
1030     if proc.returncode == 12:
1031         logging.critical("Fatal: No space left on device")
1032         cleanup()
1033         sys.exit(1)
1034
1035     if proc.returncode != 0:
1036         logging.critical("Fatal: could not install %s", source)
1037         cleanup()
1038         sys.exit(1)
1039
1040
1041 def write_uuid(target_file):
1042     """Generates an returns uuid and write it to the specified file
1043
1044     @target_file: filename to write the uuid to
1045     """
1046
1047     fileh = open(target_file, "w")
1048     uid = str(uuid.uuid4())
1049     fileh.write(uid)
1050     fileh.close()
1051     return uid
1052
1053
1054 def get_uuid(target):
1055     """Get the uuid of the specified target. Will generate an uuid if none exist.
1056
1057     @target: directory/mountpoint containing the grml layout
1058     """
1059
1060     conf_target = target + "/conf/"
1061     uuid_file_name = conf_target + "/bootid.txt"
1062     if os.path.isdir(conf_target):
1063         if os.path.isfile(uuid_file_name):
1064             uuid_file = open(uuid_file_name, "r")
1065             uid = uuid_file.readline().strip()
1066             uuid_file.close()
1067             return uid
1068         else:
1069             return write_uuid(uuid_file_name)
1070     else:
1071         execute(mkdir, conf_target)
1072         return write_uuid(uuid_file_name)
1073
1074
1075 def get_shortname(grml_flavour):
1076     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
1077     @grml_flavour: flavour name which shold be translated to shortname"""
1078
1079     return re.sub(r"[,._-]", "", grml_flavour)
1080
1081
1082 def copy_system_files(grml_flavour, iso_mount, target):
1083     """copy grml's main files (like squashfs, kernel and initrd) to a given target
1084
1085     @grml_flavour: name of grml flavour the configuration should be generated for
1086     @iso_mount: path where a grml ISO is mounted on
1087     @target: path where grml's main files should be copied to"""
1088
1089     squashfs = search_file(grml_flavour + ".squashfs", iso_mount)
1090     if squashfs is None:
1091         logging.error("error locating squashfs file")
1092         raise CriticalException(
1093             "squashfs file not found, please check that your iso is not corrupt"
1094         )
1095     else:
1096         squashfs_target = target + "/live/" + grml_flavour + "/"
1097         execute(mkdir, squashfs_target)
1098     exec_rsync(squashfs, squashfs_target + grml_flavour + ".squashfs")
1099
1100     for prefix in grml_flavour + "/", "":
1101         filesystem_module = search_file(prefix + "filesystem.module", iso_mount)
1102         if filesystem_module:
1103             break
1104     if filesystem_module is None:
1105         logging.error("error locating filesystem.module file")
1106         raise CriticalException("filesystem.module not found")
1107     else:
1108         exec_rsync(filesystem_module, squashfs_target + "filesystem.module")
1109
1110     shortname = get_shortname(grml_flavour)
1111     if os.path.isdir(iso_mount + "/boot/" + shortname):
1112         exec_rsync(iso_mount + "/boot/" + shortname, target + "/boot")
1113     else:
1114         kernel = search_file("vmlinuz", iso_mount)
1115         if kernel is None:
1116             # compat for releases < 2011.12
1117             kernel = search_file("linux26", iso_mount)
1118
1119         if kernel is None:
1120             logging.error("error locating kernel file")
1121             raise CriticalException("Kernel not found")
1122
1123         source = os.path.dirname(kernel) + "/"
1124         dest = target + "/" + os.path.dirname(kernel).replace(iso_mount, "") + "/"
1125         execute(mkdir, dest)
1126         exec_rsync(source, dest)
1127
1128
1129 def update_grml_versions(iso_mount, target):
1130     """Update the grml version file on a cd
1131     Returns true if version was updated successfully,
1132     False if grml-version does not exist yet on the mountpoint
1133
1134     @iso_mount: string of the iso mount point
1135     @target: path of the target mount point
1136     """
1137     grml_target = target + "/grml/"
1138     target_grml_version_file = search_file("grml-version", grml_target)
1139     if target_grml_version_file:
1140         iso_grml_version_file = search_file("grml-version", iso_mount)
1141         if not iso_grml_version_file:
1142             logging.warn(
1143                 "Warning: %s could not be found - can not install it",
1144                 iso_grml_version_file,
1145             )
1146             return False
1147         try:
1148             # read the flavours from the iso image
1149             iso_versions = {}
1150             iso_file = open(iso_grml_version_file, "r")
1151             for line in iso_file:
1152                 iso_versions[get_flavour(line)] = line.strip()
1153
1154             # update the existing flavours on the target
1155             for line in fileinput.input([target_grml_version_file], inplace=1):
1156                 flavour = get_flavour(line)
1157                 if flavour in list(iso_versions.keys()):
1158                     print(iso_versions.pop(flavour))
1159                 else:
1160                     print(line.strip())
1161             fileinput.close()
1162
1163             target_file = open(target_grml_version_file, "a")
1164             # add the new flavours from the current iso
1165             for flavour in iso_versions:
1166                 target_file.write("%s\n" % iso_versions[flavour])
1167         except IOError:
1168             logging.warn("Warning: Could not write file")
1169         finally:
1170             iso_file.close()
1171             target_file.close()
1172         return True
1173     else:
1174         return False
1175
1176
1177 def copy_grml_files(grml_flavour, iso_mount, target):
1178     """copy some minor grml files to a given target
1179
1180     @grml_flavour: the current grml_flavour
1181     @iso_mount: path where a grml ISO is mounted on
1182     @target: path where grml's main files should be copied to"""
1183
1184     grml_target = target + "/grml/"
1185     execute(mkdir, grml_target)
1186
1187     grml_prefixe = ["GRML", "grml"]
1188     for prefix in grml_prefixe:
1189         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
1190         if os.path.exists(filename):
1191             exec_rsync(filename, grml_target)
1192             break
1193     else:
1194         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
1195
1196
1197 def copy_addons(iso_mount, target):
1198     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1199
1200     @iso_mount: path where a grml ISO is mounted on
1201     @target: path where grml's main files should be copied to"""
1202
1203     addons = target + "/boot/addons/"
1204     execute(mkdir, addons)
1205
1206     for addon_file in glob.glob(iso_mount + "/boot/addons/*"):
1207         filename = os.path.basename(addon_file)
1208         src_file = iso_mount + "/boot/addons/" + os.path.basename(addon_file)
1209         logging.debug("Copying addon file %s" % filename)
1210         exec_rsync(src_file, addons)
1211
1212
1213 def build_loopbackcfg(target):
1214     """Generate GRUB's loopback.cfg based on existing config files.
1215
1216     @target: target directory
1217     """
1218
1219     grub_dir = "/boot/grub/"
1220     mkdir(os.path.join(target, grub_dir))
1221
1222     f = open(target + grub_dir + "loopback.cfg", "w")
1223
1224     f.write("# grml2usb generated grub2 configuration file\n")
1225     f.write("source /boot/grub/header.cfg\n")
1226
1227     for defaults in glob.glob(
1228         target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"
1229     ):
1230         sourcefile = defaults.split(target + os.path.sep)[1]
1231         logging.debug("Found source file" + sourcefile)
1232         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1233
1234     for ops in glob.glob(
1235         target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"
1236     ):
1237         sourcefile = ops.split(target + os.path.sep)[1]
1238         logging.debug("Found source file" + sourcefile)
1239         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1240
1241     f.write("source /boot/grub/addons.cfg\n")
1242     f.write("source /boot/grub/footer.cfg\n")
1243     f.close()
1244
1245
1246 def glob_and_copy(filepattern, dst):
1247     """Glob on specified filepattern and copy the result to dst
1248
1249     @filepattern: globbing pattern
1250     @dst: target directory
1251     """
1252     for name in glob.glob(filepattern):
1253         copy_if_exist(name, dst)
1254
1255
1256 def search_and_copy(filename, search_path, dst):
1257     """Search for the specified filename at searchpath and copy it to dst
1258
1259     @filename: filename to look for
1260     @search_path: base search file
1261     @dst: destionation to copy the file to
1262     """
1263     file_location = search_file(filename, search_path)
1264     copy_if_exist(file_location, dst)
1265
1266
1267 def copy_if_exist(filename, dst):
1268     """Copy filename to dst if filename is set.
1269
1270     @filename: a filename
1271     @dst: dst file
1272     """
1273     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1274         exec_rsync(filename, dst)
1275
1276
1277 def copy_bootloader_files(iso_mount, target, grml_flavour):
1278     """Copy grml's bootloader files to a given target
1279
1280     @iso_mount: path where a grml ISO is mounted on
1281     @target: path where grml's main files should be copied to
1282     @grml_flavour: name of the current processed grml_flavour
1283     """
1284
1285     syslinux_target = target + "/boot/syslinux/"
1286     execute(mkdir, syslinux_target)
1287
1288     grub_target = target + "/boot/grub/"
1289     execute(mkdir, grub_target)
1290
1291     logo = search_file("logo.16", iso_mount, required=True)
1292     exec_rsync(logo, syslinux_target + "logo.16")
1293
1294     bootx64_efi = search_file("bootx64.efi", iso_mount)
1295     if bootx64_efi:
1296         mkdir(target + "/efi/boot/")
1297         exec_rsync(bootx64_efi, target + "/efi/boot/bootx64.efi")
1298
1299     efi_img = search_file("efi.img", iso_mount)
1300     if efi_img:
1301         mkdir(target + "/boot/")
1302         exec_rsync(efi_img, target + "/boot/efi.img")
1303         handle_secure_boot(target, efi_img)
1304
1305     for ffile in ["f%d" % number for number in range(1, 11)]:
1306         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1307
1308     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1309     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1310     if os.path.isfile(syslinux_target + "ldlinux.sys"):
1311         os.unlink(syslinux_target + "ldlinux.sys")
1312
1313     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1314     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1315
1316     if not source_dir:
1317         raise CriticalException(
1318             "file default.cfg could not be found.\n"
1319             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1320             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version."
1321         )
1322
1323     if not os.path.exists(iso_mount + "/boot/grub/footer.cfg"):
1324         logging.warning(
1325             "Warning: Grml releases older than 2011.12 support only one flavour in grub."
1326         )
1327
1328     for expr in (
1329         name,
1330         "distri.cfg",
1331         defaults_file,
1332         "grml.png",
1333         "hd.cfg",
1334         "isolinux.cfg",
1335         "isolinux.bin",
1336         "isoprompt.cfg",
1337         "options.cfg",
1338         "prompt.cfg",
1339         "vesamenu.cfg",
1340         "grml.png",
1341         "*.c32",
1342     ):
1343         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1344
1345     for modules_dir in options.syslinuxlibs + SYSLINUX_LIBS:
1346         if os.path.isdir(modules_dir):
1347             for filename in glob.glob1(syslinux_target, "*.c32"):
1348                 copy_if_exist(os.path.join(modules_dir, filename), syslinux_target)
1349             break
1350
1351     # copy the addons_*.cfg file to the new syslinux directory
1352     glob_and_copy(iso_mount + source_dir + "addon*.cfg", syslinux_target)
1353
1354     search_and_copy(
1355         "hidden.cfg", iso_mount + source_dir, syslinux_target + "new_" + "hidden.cfg"
1356     )
1357
1358     # copy all grub files from ISO
1359     glob_and_copy(iso_mount + "/boot/grub/*", grub_target)
1360
1361     # finally (after all GRUB files have been installed) build static loopback.cfg
1362     build_loopbackcfg(target)
1363
1364
1365 def install_iso_files(grml_flavour, iso_mount, device, target):
1366     """Copy files from ISO to given target
1367
1368     @grml_flavour: name of grml flavour the configuration should be generated for
1369     @iso_mount: path where a grml ISO is mounted on
1370     @device: device/partition where bootloader should be installed to
1371     @target: path where grml's main files should be copied to"""
1372
1373     global GRML_DEFAULT
1374     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1375     if options.dryrun:
1376         return 0
1377     elif not options.bootloaderonly:
1378         logging.info("Copying files. This might take a while....")
1379         try:
1380             copy_system_files(grml_flavour, iso_mount, target)
1381             copy_grml_files(grml_flavour, iso_mount, target)
1382         except CriticalException as error:
1383             logging.critical("Execution failed: %s", error)
1384             sys.exit(1)
1385
1386     if not options.skipaddons:
1387         if not search_file("addons", iso_mount):
1388             logging.info("Could not find addons, therefore not installing.")
1389         else:
1390             copy_addons(iso_mount, target)
1391
1392     if not options.copyonly:
1393         copy_bootloader_files(iso_mount, target, grml_flavour)
1394
1395         if not options.dryrun:
1396             handle_bootloader_config(grml_flavour, device, target)
1397
1398     # make sure we sync filesystems before returning
1399     logging.info("Synching data (this might take a while)")
1400     proc = subprocess.Popen(["sync"])
1401     proc.wait()
1402
1403
1404 def get_device_from_partition(partition):
1405     device = partition
1406     partition_number = None
1407     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1408         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1409         if m:
1410             device = m.group(1)
1411             partition_number = int(m.group(2)) - 1
1412     return (device, partition_number)
1413
1414
1415 def get_flavour(flavour_str):
1416     """Returns the flavour of a grml version string
1417     """
1418     return re.match(r"[\w-]*", flavour_str).group()
1419
1420
1421 def identify_grml_flavour(mountpath):
1422     """Get name of grml flavour
1423
1424     @mountpath: path where the grml ISO is mounted to
1425     @return: name of grml-flavour"""
1426
1427     version_files = search_file("grml-version", mountpath, lst_return=True)
1428
1429     if not version_files:
1430         if mountpath.startswith("/run/live/medium"):
1431             logging.critical("Error: could not find grml-version file.")
1432             logging.critical(
1433                 "Looks like your system is running from RAM but required files are not available."
1434             )
1435             logging.critical(
1436                 "Please either boot without toram=... or use boot option toram instead of toram=..."
1437             )
1438         else:
1439             logging.critical("Error: could not find grml-version file.")
1440         cleanup()
1441         sys.exit(1)
1442
1443     flavours = []
1444     logging.debug("version_files = %s", version_files)
1445     for version_file in version_files:
1446         tmpfile = None
1447         try:
1448             tmpfile = open(version_file, "r")
1449             for line in tmpfile.readlines():
1450                 flavours.append(get_flavour(line))
1451         finally:
1452             if tmpfile:
1453                 tmpfile.close()
1454
1455     return flavours
1456
1457
1458 def get_bootoptions(grml_flavour):
1459     """Returns bootoptions for specific flavour
1460
1461     @grml_flavour: name of the grml_flavour
1462     """
1463     # do NOT write "None" in kernel cmdline
1464     if not options.bootoptions:
1465         bootopt = ""
1466     else:
1467         bootopt = " ".join(options.bootoptions)
1468     bootopt = bootopt.replace("%flavour", grml_flavour)
1469     return bootopt
1470
1471
1472 def handle_grub_config(grml_flavour, device, target):
1473     """Main handler for generating grub (v1 and v2) configuration
1474
1475     @grml_flavour: name of grml flavour the configuration should be generated for
1476     @device: device/partition where grub should be installed to
1477     @target: path of grub's configuration files"""
1478
1479     global UUID
1480
1481     logging.debug("Updating grub configuration")
1482
1483     grub_target = target + "/boot/grub/"
1484     secureboot_target = target + "/EFI/ubuntu/"
1485
1486     bootid_re = re.compile(r"bootid=[\w_-]+")
1487     live_media_path_re = re.compile(r"live-media-path=[\w_/-]+")
1488
1489     bootopt = get_bootoptions(grml_flavour)
1490
1491     remove_regexes = []
1492     option_re = re.compile(r"(.*/boot/.*(linux26|vmlinuz).*)")
1493
1494     if options.removeoption:
1495         for regex in options.removeoption:
1496             remove_regexes.append(re.compile(regex))
1497
1498     shortname = get_shortname(grml_flavour)
1499     for filename in glob.glob(grub_target + "*.cfg") + glob.glob(
1500         secureboot_target + "*.cfg"
1501     ):
1502         for line in fileinput.input(filename, inplace=1):
1503             line = line.rstrip("\r\n")
1504             if option_re.search(line):
1505                 line = bootid_re.sub("", line)
1506                 if shortname in filename:
1507                     line = live_media_path_re.sub("", line)
1508                     line = line.rstrip() + " live-media-path=/live/%s/ " % (
1509                         grml_flavour
1510                     )
1511                 if bootopt.strip():
1512                     line = line.replace(" {} ".format(bootopt.strip()), " ")
1513                     if line.endswith(bootopt):
1514                         line = line[: -len(bootopt)]
1515                 line = line.rstrip() + r" bootid=%s %s " % (UUID, bootopt)
1516                 for regex in remove_regexes:
1517                     line = regex.sub(" ", line)
1518             print(line)
1519         fileinput.close()
1520
1521
1522 def initial_syslinux_config(target):
1523     """Generates initial syslinux configuration
1524
1525     @target path of syslinux's configuration files"""
1526
1527     target = target + "/"
1528     filename = target + "grmlmain.cfg"
1529     if os.path.isfile(target + "grmlmain.cfg"):
1530         return
1531     data = open(filename, "w")
1532     data.write(generate_main_syslinux_config())
1533     data.close()
1534
1535     filename = target + "hiddens.cfg"
1536     data = open(filename, "w")
1537     data.write("include hidden.cfg\n")
1538     data.close()
1539
1540
1541 def add_entry_if_not_present(filename, entry):
1542     """Write entry into filename if entry is not already in the file
1543
1544     @filename: name of the file
1545     @entry: data to write to the file
1546     """
1547     data = open(filename, "a+")
1548     data.seek(0)
1549     for line in data:
1550         if line == entry:
1551             break
1552     else:
1553         data.write(entry)
1554
1555     data.close()
1556
1557
1558 def get_flavour_filename(flavour):
1559     """Generate a iso9960 save filename out of the specified flavour
1560
1561     @flavour: grml flavour
1562     """
1563     return flavour.replace("-", "_")
1564
1565
1566 def adjust_syslinux_bootoptions(src, flavour):
1567     """Adjust existing bootoptions of specified syslinux config to
1568     grml2usb specific ones, e.g. change the location of the kernel...
1569
1570     @src: config file to alter
1571     @flavour: grml flavour
1572     """
1573
1574     append_re = re.compile(r"^(\s*append.*/boot/.*)$", re.I)
1575     # flavour_re = re.compile("(label.*)(grml\w+)")
1576     default_re = re.compile(r"(default.cfg)")
1577     bootid_re = re.compile(r"bootid=[\w_-]+")
1578     live_media_path_re = re.compile(r"live-media-path=[\w_/-]+")
1579
1580     bootopt = get_bootoptions(flavour)
1581
1582     regexe = []
1583     option_re = None
1584     if options.removeoption:
1585         option_re = re.compile(r"/boot/.*/(initrd.gz|initrd.img)")
1586
1587         for regex in options.removeoption:
1588             regexe.append(re.compile(r"%s" % regex))
1589
1590     for line in fileinput.input(src, inplace=1):
1591         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1592         line = default_re.sub(r"%s-\1" % flavour, line)
1593         line = bootid_re.sub("", line)
1594         line = live_media_path_re.sub("", line)
1595         line = append_re.sub(r"\1 live-media-path=/live/%s/ " % flavour, line)
1596         line = append_re.sub(r"\1 boot=live %s " % bootopt, line)
1597         line = append_re.sub(r"\1 %s=%s " % ("bootid", UUID), line)
1598         if option_re and option_re.search(line):
1599             for regex in regexe:
1600                 line = regex.sub(" ", line)
1601         sys.stdout.write(line)
1602     fileinput.close()
1603
1604
1605 def adjust_labels(src, replacement):
1606     """Adjust the specified labels in the syslinux config file src with
1607     specified replacement
1608     """
1609     label_re = re.compile(r"^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1610     for line in fileinput.input(src, inplace=1):
1611         line = label_re.sub(replacement, line)
1612         sys.stdout.write(line)
1613     fileinput.close()
1614
1615
1616 def add_syslinux_entry(filename, grml_flavour):
1617     """Add includes for a specific grml_flavour to the specified filename
1618
1619     @filename: syslinux config file
1620     @grml_flavour: grml flavour to add
1621     """
1622
1623     entry_filename = "option_%s.cfg" % grml_flavour
1624     entry = "include %s\n" % entry_filename
1625
1626     add_entry_if_not_present(filename, entry)
1627     path = os.path.dirname(filename)
1628
1629     data = open(path + "/" + entry_filename, "w")
1630     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1631     data.close()
1632
1633
1634 def modify_filenames(grml_flavour, target, filenames):
1635     """Replace the standard filenames with the new ones
1636
1637     @grml_flavour: grml-flavour strin
1638     @target: directory where the files are located
1639     @filenames: list of filenames to alter
1640     """
1641     grml_filename = get_flavour_filename(grml_flavour)
1642     for filename in filenames:
1643         old_filename = "%s/%s" % (target, filename)
1644         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1645         os.rename(old_filename, new_filename)
1646
1647
1648 def remove_default_entry(filename):
1649     """Remove the default entry from specified syslinux file
1650
1651     @filename: syslinux config file
1652     """
1653     default_re = re.compile(r"^(\s*menu\s*default\s*)$", re.I)
1654     for line in fileinput.input(filename, inplace=1):
1655         if default_re.match(line):
1656             continue
1657         sys.stdout.write(line)
1658     fileinput.close()
1659
1660
1661 def handle_syslinux_config(grml_flavour, target):
1662     """Main handler for generating syslinux configuration
1663
1664     @grml_flavour: name of grml flavour the configuration should be generated for
1665     @target: path of syslinux's configuration files"""
1666
1667     logging.debug("Generating syslinux configuration")
1668     syslinux_target = target + "/boot/syslinux/"
1669     # should be present via  copy_bootloader_files(), but make sure it exits:
1670     execute(mkdir, syslinux_target)
1671     syslinux_cfg = syslinux_target + "syslinux.cfg"
1672
1673     # install main configuration only *once*, no matter how many ISOs we have:
1674     syslinux_config_file = open(syslinux_cfg, "w")
1675     syslinux_config_file.write("timeout 300\n")
1676     syslinux_config_file.write("include vesamenu.cfg\n")
1677     syslinux_config_file.close()
1678
1679     prompt_name = open(syslinux_target + "promptname.cfg", "w")
1680     prompt_name.write("menu label S^yslinux prompt\n")
1681     prompt_name.close()
1682
1683     initial_syslinux_config(syslinux_target)
1684     flavour_filename = get_flavour_filename(grml_flavour)
1685
1686     if search_file("default.cfg", syslinux_target):
1687         modify_filenames(grml_flavour, syslinux_target, ["grml.cfg", "default.cfg"])
1688
1689     filename = search_file("new_hidden.cfg", syslinux_target)
1690
1691     # process hidden file
1692     if not search_file("hidden.cfg", syslinux_target):
1693         new_hidden = syslinux_target + "hidden.cfg"
1694         os.rename(filename, new_hidden)
1695         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1696     else:
1697         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1698         os.rename(filename, new_hidden_file)
1699         adjust_labels(new_hidden_file, r"\1 %s-\2" % grml_flavour)
1700         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1701         entry = "include %s_hidden.cfg\n" % flavour_filename
1702         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1703
1704     new_default = "%s_default.cfg" % (flavour_filename)
1705     entry = "include %s\n" % new_default
1706     defaults_file = "%s/defaults.cfg" % syslinux_target
1707     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1708     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1709
1710     if os.path.isfile(defaults_file):
1711
1712         # remove default menu entry in menu
1713         remove_default_entry(new_default_with_path)
1714
1715         # adjust all labels for additional isos
1716         adjust_labels(new_default_with_path, r"\1 %s" % grml_flavour)
1717         adjust_labels(new_grml_cfg, r"\1 %s-\2" % grml_flavour)
1718
1719     # always adjust bootoptions
1720     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1721     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1722
1723     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1724
1725     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1726
1727
1728 def handle_secure_boot(target, efi_img):
1729     """Provide secure boot support by extracting files from /boot/efi.img
1730
1731     @target: path where grml's main files should be copied to
1732     @efi_img: path to the efi.img file that includes the files for secure boot
1733     """
1734
1735     mkdir(target + "/efi/boot/")
1736     efi_mountpoint = tempfile.mkdtemp(
1737         prefix="grml2usb", dir=os.path.abspath(options.tmpdir)
1738     )
1739     logging.debug("efi_mountpoint = %s" % efi_mountpoint)
1740     register_tmpfile(efi_mountpoint)
1741
1742     try:
1743         logging.debug(
1744             "mount(%s, %s, ['-o', 'ro', '-t', 'vfat']" % (efi_img, efi_mountpoint)
1745         )
1746         mount(efi_img, efi_mountpoint, ["-o", "ro", "-t", "vfat"])
1747     except CriticalException as error:
1748         logging.critical("Fatal: %s", error)
1749         sys.exit(1)
1750
1751     ubuntu_cfg = search_file("grub.cfg", efi_mountpoint + "/EFI/ubuntu")
1752     logging.debug("ubuntu_cfg = %s" % ubuntu_cfg)
1753     if not ubuntu_cfg:
1754         logging.info(
1755             "No /EFI/ubuntu/grub.cfg found inside EFI image, looks like Secure Boot support is missing."
1756         )
1757     else:
1758         mkdir(target + "/efi/ubuntu")
1759         logging.debug(
1760             "exec_rsync(%s, %s + '/efi/ubuntu/grub.cfg')" % (ubuntu_cfg, target)
1761         )
1762         exec_rsync(ubuntu_cfg, target + "/efi/ubuntu/grub.cfg")
1763
1764         logging.debug(
1765             "exec_rsync(%s + '/EFI/BOOT/grubx64.efi', %s + '/efi/boot/grubx64.efi')'"
1766             % (efi_mountpoint, target)
1767         )
1768         exec_rsync(
1769             efi_mountpoint + "/EFI/BOOT/grubx64.efi", target + "/efi/boot/grubx64.efi"
1770         )
1771
1772         # NOTE - we're overwriting /efi/boot/bootx64.efi from copy_bootloader_files here
1773         logging.debug(
1774             "exec_rsync(%s + '/EFI/BOOT/bootx64.efi', %s + '/efi/boot/bootx64.efi')'"
1775             % (efi_mountpoint, target)
1776         )
1777         exec_rsync(
1778             efi_mountpoint + "/EFI/BOOT/bootx64.efi", target + "/efi/boot/bootx64.efi"
1779         )
1780
1781     try:
1782         unmount(efi_mountpoint, "")
1783         logging.debug("Unmounted %s" % efi_mountpoint)
1784         os.rmdir(efi_mountpoint)
1785         logging.debug("Removed directory %s" % efi_mountpoint)
1786     except Exception:
1787         logging.critical("RuntimeError while umount %s" % efi_mountpoint)
1788         sys.exit(1)
1789
1790
1791 def handle_bootloader_config(grml_flavour, device, target):
1792     """Main handler for generating bootloader's configuration
1793
1794     @grml_flavour: name of grml flavour the configuration should be generated for
1795     @device: device/partition where bootloader should be installed to
1796     @target: path of bootloader's configuration files"""
1797
1798     global UUID
1799     UUID = get_uuid(target)
1800     if options.skipsyslinuxconfig:
1801         logging.info("Skipping generation of syslinux configuration as requested.")
1802     else:
1803         try:
1804             handle_syslinux_config(grml_flavour, target)
1805         except CriticalException as error:
1806             logging.critical("Fatal: %s", error)
1807             sys.exit(1)
1808
1809     if options.skipgrubconfig:
1810         logging.info("Skipping generation of grub configuration as requested.")
1811     else:
1812         try:
1813             handle_grub_config(grml_flavour, device, target)
1814         except CriticalException as error:
1815             logging.critical("Fatal: %s", error)
1816             sys.exit(1)
1817
1818
1819 def install(image, device):
1820     """Install a grml image to the specified device
1821
1822     @image: directory or is file
1823     @device: partition or directory to install the device
1824     """
1825     iso_mountpoint = image
1826     remove_image_mountpoint = False
1827     if os.path.isdir(image):
1828         if options.force or os.path.exists(os.path.join(image, "live")):
1829             logging.info("Using %s as install base", image)
1830         else:
1831             q = input(
1832                 "%s does not look like a Grml system. "
1833                 "Do you really want to use this image? y/N " % image
1834             )
1835             if q.lower() == "y":
1836                 logging.info("Using %s as install base", image)
1837             else:
1838                 logging.info("Skipping install base %s", image)
1839     else:
1840         logging.info("Using ISO %s", image)
1841         iso_mountpoint = tempfile.mkdtemp(
1842             prefix="grml2usb", dir=os.path.abspath(options.tmpdir)
1843         )
1844         register_tmpfile(iso_mountpoint)
1845         remove_image_mountpoint = True
1846         try:
1847             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1848         except CriticalException as error:
1849             logging.critical("Fatal: %s", error)
1850             sys.exit(1)
1851
1852     try:
1853         install_grml(iso_mountpoint, device)
1854     finally:
1855         if remove_image_mountpoint:
1856             try:
1857                 remove_mountpoint(iso_mountpoint)
1858             except CriticalException:
1859                 cleanup()
1860                 raise
1861
1862
1863 def install_grml(mountpoint, device):
1864     """Main logic for copying files of the currently running Grml system.
1865
1866     @mountpoint: directory where currently running live system resides (usually /run/live/medium)
1867     @device: partition where the specified ISO should be installed to"""
1868
1869     device_mountpoint = device
1870     if os.path.isdir(device):
1871         logging.info("Specified device is a directory, therefore not mounting.")
1872         remove_device_mountpoint = False
1873     else:
1874         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1875         register_tmpfile(device_mountpoint)
1876         remove_device_mountpoint = True
1877         try:
1878             check_for_fat(device)
1879             if not options.skipbootflag:
1880                 check_boot_flag(device)
1881
1882             set_rw(device)
1883             mount(device, device_mountpoint, ["-o", "utf8,iocharset=iso8859-1"])
1884         except CriticalException:
1885             mount(device, device_mountpoint, "")
1886     try:
1887         grml_flavours = identify_grml_flavour(mountpoint)
1888         for flavour in set(grml_flavours):
1889             if not flavour:
1890                 logging.warning("No valid flavour found, please check your iso")
1891             logging.info('Identified grml flavour "%s".', flavour)
1892             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1893             GRML_FLAVOURS.add(flavour)
1894     finally:
1895         if remove_device_mountpoint:
1896             remove_mountpoint(device_mountpoint)
1897
1898
1899 def remove_mountpoint(mountpoint):
1900     """remove a registered mountpoint
1901     """
1902
1903     try:
1904         unmount(mountpoint, "")
1905         if os.path.isdir(mountpoint):
1906             os.rmdir(mountpoint)
1907             unregister_tmpfile(mountpoint)
1908     except CriticalException:
1909         cleanup()
1910         raise
1911
1912
1913 def handle_mbr(device):
1914     """Main handler for installing master boot record (MBR)
1915
1916     @device: device where the MBR should be installed to"""
1917
1918     if options.dryrun:
1919         logging.info("Would install MBR")
1920         return 0
1921
1922     mbr_device, partition_number = get_device_from_partition(device)
1923     if partition_number is None:
1924         logging.warn("Could not detect partition number, not activating partition")
1925
1926     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1927     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1928     if mbr_device == "/dev/loop":
1929         mbr_device = device
1930         logging.info(
1931             "Detected loop device - using %s as MBR device therefore", mbr_device
1932         )
1933
1934     mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1935     if options.syslinuxmbr:
1936         mbrcode = ""
1937         mbr_locations = (
1938             "/usr/lib/syslinux/mbr.bin",
1939             "/usr/lib/syslinux/bios/mbr.bin",
1940             "/usr/share/syslinux/mbr.bin",
1941         )
1942         for mbrpath in mbr_locations:
1943             if os.path.isfile(mbrpath):
1944                 mbrcode = mbrpath
1945                 break
1946
1947         if not mbrcode:
1948             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1949             logging.error("Cannot find syslinux MBR, install it at %s)", str_locations)
1950             raise CriticalException(
1951                 "syslinux MBR  can not be found at %s." % str_locations
1952             )
1953     elif options.mbrmenu:
1954         mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1955
1956     try:
1957         install_mbr(mbrcode, mbr_device, partition_number, True)
1958     except (IOError, Exception) as error:
1959         logging.critical("Execution failed: %s", error)
1960         sys.exit(1)
1961
1962
1963 def handle_vfat(device):
1964     """Check for FAT specific settings and options
1965
1966     @device: device that should checked / formated"""
1967
1968     # make sure we have mkfs.vfat available
1969     if options.fat16:
1970         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1971             logging.critical("Sorry, mkfs.vfat not available. Exiting.")
1972             logging.critical("Please make sure to install dosfstools.")
1973             sys.exit(1)
1974
1975         if options.force:
1976             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1977         else:
1978             # make sure the user is aware of what he is doing
1979             f = input(
1980                 "Are you sure you want to format the specified partition with fat16? y/N "
1981             )
1982             if f == "y" or f == "Y":
1983                 logging.info(
1984                     "Note: you can skip this question using the option --force"
1985                 )
1986             else:
1987                 sys.exit(1)
1988         try:
1989             mkfs_fat16(device)
1990         except CriticalException as error:
1991             logging.critical("Execution failed: %s", error)
1992             sys.exit(1)
1993
1994     # check for vfat filesystem
1995     if device is not None and not os.path.isdir(device) and options.syslinux:
1996         try:
1997             check_for_fat(device)
1998         except CriticalException as error:
1999             logging.critical("Execution failed: %s", error)
2000             sys.exit(1)
2001
2002     if options.skipusbcheck:
2003         logging.info(
2004             "Not checking for removable USB device as requested via option --skip-usb-check."
2005         )
2006         return
2007
2008     if (
2009         not os.path.isdir(device)
2010         and not check_for_usbdevice(device)
2011         and not options.force
2012     ):
2013         print(
2014             "Warning: the specified device %s does not look like a removable usb device."
2015             % device
2016         )
2017         f = input("Do you really want to continue? y/N ")
2018         if f.lower() != "y":
2019             sys.exit(1)
2020
2021
2022 def handle_compat_warning(device):
2023     """Backwards compatible checks
2024
2025     @device: device that should be checked"""
2026
2027     # make sure we can replace old grml2usb script and warn user when using old way of life:
2028     if (
2029         device.startswith("/mnt/external")
2030         or device.startswith("/mnt/usb")
2031         and not options.force
2032     ):
2033         print("Warning: the semantics of grml2usb has changed.")
2034         print("Instead of using grml2usb /path/to/iso %s you might" % device)
2035         print("want to use grml2usb /path/to/iso /dev/... instead.")
2036         print("Please check out the grml2usb manpage for details.")
2037         f = input("Do you really want to continue? y/N ")
2038         if f.lower() != "y":
2039             sys.exit(1)
2040
2041
2042 def handle_logging():
2043     """Log handling and configuration"""
2044
2045     if options.verbose and options.quiet:
2046         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
2047
2048     FORMAT = "%(message)s"
2049     if options.verbose:
2050         FORMAT = "%(asctime)-15s %(message)s"
2051         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
2052     elif options.quiet:
2053         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
2054     else:
2055         logging.basicConfig(level=logging.INFO, format=FORMAT)
2056
2057
2058 def handle_bootloader(device):
2059     """wrapper for installing bootloader
2060
2061     @device: device where bootloader should be installed to"""
2062
2063     # Install bootloader only if not using the --copy-only option
2064     if options.copyonly:
2065         logging.info(
2066             "Not installing bootloader and its files as requested via option copyonly."
2067         )
2068     elif os.path.isdir(device):
2069         logging.info("Not installing bootloader as %s is a directory.", device)
2070     else:
2071         install_bootloader(device)
2072
2073
2074 def check_options(opts):
2075     """Check compatibility of provided user opts
2076
2077     @opts option dict from OptionParser
2078     """
2079     if opts.grubmbr and not opts.grub:
2080         raise CriticalException("--grub-mbr requires --grub option.")
2081
2082     if opts.copyonly and opts.grub:
2083         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
2084
2085
2086 def check_programs():
2087     """check if all needed programs are installed"""
2088     if options.grub:
2089         global GRUB_INSTALL
2090         GRUB_INSTALL = which("grub-install") or which("grub2-install")
2091         if not GRUB_INSTALL:
2092             logging.critical(
2093                 "Fatal: grub-install not available (please install the "
2094                 "grub package or drop the --grub option)"
2095             )
2096             sys.exit(1)
2097
2098     if options.syslinux:
2099         if not which("syslinux"):
2100             logging.critical(
2101                 "Fatal: syslinux not available (please install the "
2102                 "syslinux package or use the --grub option)"
2103             )
2104             sys.exit(1)
2105
2106     if not which("rsync"):
2107         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2108         sys.exit(1)
2109
2110
2111 def load_loop():
2112     """Runs modprobe loop and throws away its output"""
2113     if not which("modprobe"):
2114         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
2115         logging.critical("Hint: is /sbin missing in PATH?")
2116         sys.exit(1)
2117
2118     proc = subprocess.Popen(
2119         ["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2120     )
2121     proc.communicate()
2122
2123
2124 def main():
2125     """Main invocation"""
2126
2127     try:
2128         if options.version:
2129             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
2130             sys.exit(0)
2131
2132         if len(args) < 2:
2133             parser.error("invalid usage")
2134
2135         # log handling
2136         handle_logging()
2137
2138         # make sure we have the appropriate permissions
2139         check_uid_root()
2140
2141         check_options(options)
2142
2143         load_loop()
2144
2145         logging.info("Executing grml2usb version %s", PROG_VERSION)
2146
2147         if options.dryrun:
2148             logging.info("Running in simulation mode as requested via option dry-run.")
2149
2150         check_programs()
2151
2152         # specified arguments
2153         device = os.path.realpath(args[len(args) - 1])
2154         isos = args[0 : len(args) - 1]
2155
2156         if not os.path.isdir(device):
2157             if device[-1:].isdigit():
2158                 if int(device[-1:]) > 4 or device[-2:].isdigit():
2159                     logging.warn(
2160                         "Warning: installing on partition number >4, booting *might* fail depending on your system."
2161                     )
2162
2163         # provide upgrade path
2164         handle_compat_warning(device)
2165
2166         # check for vfat partition
2167         handle_vfat(device)
2168
2169         # main operation (like installing files)
2170         for iso in isos:
2171             install(iso, device)
2172
2173         # install mbr
2174         is_superfloppy = not device[-1:].isdigit()
2175         if is_superfloppy:
2176             logging.info("Detected superfloppy format - not installing MBR")
2177
2178         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
2179             handle_mbr(device)
2180
2181         handle_bootloader(device)
2182
2183         logging.info(
2184             "Note: grml flavour %s was installed as the default booting system.",
2185             GRML_DEFAULT,
2186         )
2187
2188         for flavour in GRML_FLAVOURS:
2189             logging.info(
2190                 "Note: you can boot flavour %s using '%s' on the commandline.",
2191                 flavour,
2192                 flavour,
2193             )
2194
2195         # finally be polite :)
2196         logging.info(
2197             "Finished execution of grml2usb (%s). Have fun with your Grml system.",
2198             PROG_VERSION,
2199         )
2200
2201     except Exception as error:
2202         logging.critical("Fatal: %s", str(error))
2203         if options.verbose:
2204             logging.exception("Exception:")
2205         sys.exit(1)
2206
2207
2208 if __name__ == "__main__":
2209     try:
2210         main()
2211     except KeyboardInterrupt:
2212         logging.info("Received KeyboardInterrupt")
2213         cleanup()
2214
2215 # END OF FILE ##################################################################
2216 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8