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