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