70d514299e07b884044965ac83821135a3162778
[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             set_rw(device)
1879             mount(device, device_mountpoint, ["-o", "utf8,iocharset=iso8859-1"])
1880         except CriticalException:
1881             mount(device, device_mountpoint, "")
1882     try:
1883         grml_flavours = identify_grml_flavour(mountpoint)
1884         for flavour in set(grml_flavours):
1885             if not flavour:
1886                 logging.warning("No valid flavour found, please check your iso")
1887             logging.info('Identified grml flavour "%s".', flavour)
1888             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1889             GRML_FLAVOURS.add(flavour)
1890     finally:
1891         if remove_device_mountpoint:
1892             remove_mountpoint(device_mountpoint)
1893
1894
1895 def remove_mountpoint(mountpoint):
1896     """remove a registered mountpoint
1897     """
1898
1899     try:
1900         unmount(mountpoint, "")
1901         if os.path.isdir(mountpoint):
1902             os.rmdir(mountpoint)
1903             unregister_tmpfile(mountpoint)
1904     except CriticalException:
1905         cleanup()
1906         raise
1907
1908
1909 def handle_mbr(device):
1910     """Main handler for installing master boot record (MBR)
1911
1912     @device: device where the MBR should be installed to"""
1913
1914     if options.dryrun:
1915         logging.info("Would install MBR")
1916         return 0
1917
1918     mbr_device, partition_number = get_device_from_partition(device)
1919     if partition_number is None:
1920         logging.warn("Could not detect partition number, not activating partition")
1921
1922     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1923     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1924     if mbr_device == "/dev/loop":
1925         mbr_device = device
1926         logging.info(
1927             "Detected loop device - using %s as MBR device therefore", mbr_device
1928         )
1929
1930     mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1931     if options.syslinuxmbr:
1932         mbrcode = ""
1933         mbr_locations = (
1934             "/usr/lib/syslinux/mbr.bin",
1935             "/usr/lib/syslinux/bios/mbr.bin",
1936             "/usr/share/syslinux/mbr.bin",
1937         )
1938         for mbrpath in mbr_locations:
1939             if os.path.isfile(mbrpath):
1940                 mbrcode = mbrpath
1941                 break
1942
1943         if not mbrcode:
1944             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1945             logging.error("Cannot find syslinux MBR, install it at %s)", str_locations)
1946             raise CriticalException(
1947                 "syslinux MBR  can not be found at %s." % str_locations
1948             )
1949     elif options.mbrmenu:
1950         mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1951
1952     try:
1953         install_mbr(mbrcode, mbr_device, partition_number, True)
1954     except (IOError, Exception) as error:
1955         logging.critical("Execution failed: %s", error)
1956         sys.exit(1)
1957
1958
1959 def handle_vfat(device):
1960     """Check for FAT specific settings and options
1961
1962     @device: device that should checked / formated"""
1963
1964     # make sure we have mkfs.vfat available
1965     if options.fat16:
1966         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1967             logging.critical("Sorry, mkfs.vfat not available. Exiting.")
1968             logging.critical("Please make sure to install dosfstools.")
1969             sys.exit(1)
1970
1971         if options.force:
1972             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1973         else:
1974             # make sure the user is aware of what he is doing
1975             f = input(
1976                 "Are you sure you want to format the specified partition with fat16? y/N "
1977             )
1978             if f == "y" or f == "Y":
1979                 logging.info(
1980                     "Note: you can skip this question using the option --force"
1981                 )
1982             else:
1983                 sys.exit(1)
1984         try:
1985             mkfs_fat16(device)
1986         except CriticalException as error:
1987             logging.critical("Execution failed: %s", error)
1988             sys.exit(1)
1989
1990     # check for vfat filesystem
1991     if device is not None and not os.path.isdir(device) and options.syslinux:
1992         try:
1993             check_for_fat(device)
1994         except CriticalException as error:
1995             logging.critical("Execution failed: %s", error)
1996             sys.exit(1)
1997
1998     if options.skipusbcheck:
1999         logging.info(
2000             "Not checking for removable USB device as requested via option --skip-usb-check."
2001         )
2002         return
2003
2004     if (
2005         not os.path.isdir(device)
2006         and not check_for_usbdevice(device)
2007         and not options.force
2008     ):
2009         print(
2010             "Warning: the specified device %s does not look like a removable usb device."
2011             % device
2012         )
2013         f = input("Do you really want to continue? y/N ")
2014         if f.lower() != "y":
2015             sys.exit(1)
2016
2017
2018 def handle_compat_warning(device):
2019     """Backwards compatible checks
2020
2021     @device: device that should be checked"""
2022
2023     # make sure we can replace old grml2usb script and warn user when using old way of life:
2024     if (
2025         device.startswith("/mnt/external")
2026         or device.startswith("/mnt/usb")
2027         and not options.force
2028     ):
2029         print("Warning: the semantics of grml2usb has changed.")
2030         print("Instead of using grml2usb /path/to/iso %s you might" % device)
2031         print("want to use grml2usb /path/to/iso /dev/... instead.")
2032         print("Please check out the grml2usb manpage for details.")
2033         f = input("Do you really want to continue? y/N ")
2034         if f.lower() != "y":
2035             sys.exit(1)
2036
2037
2038 def handle_logging():
2039     """Log handling and configuration"""
2040
2041     if options.verbose and options.quiet:
2042         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
2043
2044     FORMAT = "%(message)s"
2045     if options.verbose:
2046         FORMAT = "%(asctime)-15s %(message)s"
2047         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
2048     elif options.quiet:
2049         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
2050     else:
2051         logging.basicConfig(level=logging.INFO, format=FORMAT)
2052
2053
2054 def handle_bootloader(device):
2055     """wrapper for installing bootloader
2056
2057     @device: device where bootloader should be installed to"""
2058
2059     # Install bootloader only if not using the --copy-only option
2060     if options.copyonly:
2061         logging.info(
2062             "Not installing bootloader and its files as requested via option copyonly."
2063         )
2064     elif os.path.isdir(device):
2065         logging.info("Not installing bootloader as %s is a directory.", device)
2066     else:
2067         install_bootloader(device)
2068
2069
2070 def check_options(opts):
2071     """Check compatibility of provided user opts
2072
2073     @opts option dict from OptionParser
2074     """
2075     if opts.grubmbr and not opts.grub:
2076         raise CriticalException("--grub-mbr requires --grub option.")
2077
2078     if opts.copyonly and opts.grub:
2079         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
2080
2081
2082 def check_programs():
2083     """check if all needed programs are installed"""
2084     if options.grub:
2085         global GRUB_INSTALL
2086         GRUB_INSTALL = which("grub-install") or which("grub2-install")
2087         if not GRUB_INSTALL:
2088             logging.critical(
2089                 "Fatal: grub-install not available (please install the "
2090                 "grub package or drop the --grub option)"
2091             )
2092             sys.exit(1)
2093
2094     if options.syslinux:
2095         if not which("syslinux"):
2096             logging.critical(
2097                 "Fatal: syslinux not available (please install the "
2098                 "syslinux package or use the --grub option)"
2099             )
2100             sys.exit(1)
2101
2102     if not which("rsync"):
2103         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2104         sys.exit(1)
2105
2106
2107 def load_loop():
2108     """Runs modprobe loop and throws away its output"""
2109     if not which("modprobe"):
2110         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
2111         logging.critical("Hint: is /sbin missing in PATH?")
2112         sys.exit(1)
2113
2114     proc = subprocess.Popen(
2115         ["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2116     )
2117     proc.communicate()
2118
2119
2120 def main():
2121     """Main invocation"""
2122
2123     try:
2124         if options.version:
2125             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
2126             sys.exit(0)
2127
2128         if len(args) < 2:
2129             parser.error("invalid usage")
2130
2131         # log handling
2132         handle_logging()
2133
2134         # make sure we have the appropriate permissions
2135         check_uid_root()
2136
2137         check_options(options)
2138
2139         load_loop()
2140
2141         logging.info("Executing grml2usb version %s", PROG_VERSION)
2142
2143         if options.dryrun:
2144             logging.info("Running in simulation mode as requested via option dry-run.")
2145
2146         check_programs()
2147
2148         # specified arguments
2149         device = os.path.realpath(args[len(args) - 1])
2150         isos = args[0 : len(args) - 1]
2151
2152         if not os.path.isdir(device):
2153             if device[-1:].isdigit():
2154                 if int(device[-1:]) > 4 or device[-2:].isdigit():
2155                     logging.warn(
2156                         "Warning: installing on partition number >4, booting *might* fail depending on your system."
2157                     )
2158
2159         # provide upgrade path
2160         handle_compat_warning(device)
2161
2162         if not options.skipbootflag:
2163             check_boot_flag(device)
2164
2165         # check for vfat partition
2166         handle_vfat(device)
2167
2168         # main operation (like installing files)
2169         for iso in isos:
2170             install(iso, device)
2171
2172         # install mbr
2173         is_superfloppy = not device[-1:].isdigit()
2174         if is_superfloppy:
2175             logging.info("Detected superfloppy format - not installing MBR")
2176
2177         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
2178             handle_mbr(device)
2179
2180         handle_bootloader(device)
2181
2182         logging.info(
2183             "Note: grml flavour %s was installed as the default booting system.",
2184             GRML_DEFAULT,
2185         )
2186
2187         for flavour in GRML_FLAVOURS:
2188             logging.info(
2189                 "Note: you can boot flavour %s using '%s' on the commandline.",
2190                 flavour,
2191                 flavour,
2192             )
2193
2194         # finally be polite :)
2195         logging.info(
2196             "Finished execution of grml2usb (%s). Have fun with your Grml system.",
2197             PROG_VERSION,
2198         )
2199
2200     except Exception as error:
2201         logging.critical("Fatal: %s", str(error))
2202         if options.verbose:
2203             logging.exception("Exception:")
2204         sys.exit(1)
2205
2206
2207 if __name__ == "__main__":
2208     try:
2209         main()
2210     except KeyboardInterrupt:
2211         logging.info("Received KeyboardInterrupt")
2212         cleanup()
2213
2214 # END OF FILE ##################################################################
2215 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8