Code cleanups
[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", "-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 check_for_usbdevice(device):
910     """Check whether the specified device is a removable USB device
911
912     @device: device name, like /dev/sda1 or /dev/sda
913     """
914
915     usbdevice = re.match(r"/dev/(.*?)\d*$", device).group(1)
916     # newer systems:
917     usbdev = os.path.realpath("/sys/class/block/" + usbdevice + "/removable")
918     if not os.path.isfile(usbdev):
919         # Ubuntu with kernel 2.6.24 for example:
920         usbdev = os.path.realpath("/sys/block/" + usbdevice + "/removable")
921
922     if os.path.isfile(usbdev):
923         is_usb = open(usbdev).readline()
924         if is_usb.find("1"):
925             return 0
926
927     return 1
928
929
930 def check_for_fat(partition):
931     """Check whether specified partition is a valid VFAT/FAT16 filesystem
932
933     @partition: device name of partition"""
934
935     if not os.access(partition, os.R_OK):
936         raise CriticalException(
937             "Failed to read device %s"
938             " (wrong UID/permissions or device/directory not present?)" % partition
939         )
940
941     try:
942         filesystem = (
943             subprocess.check_output(
944                 ["/sbin/blkid", "-s", "TYPE", "-o", "value", partition]
945             )
946             .decode()
947             .rstrip()
948         )
949
950         if filesystem != "vfat":
951             raise CriticalException(
952                 "Partition %s does not contain a FAT16 filesystem. "
953                 "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition)
954             )
955
956     except OSError:
957         raise CriticalException(
958             "Sorry, /sbin/blkid not available (install util-linux?)"
959         )
960
961
962 def mkdir(directory):
963     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
964
965     # just silently pass as it's just fine it the directory exists
966     if not os.path.isdir(directory):
967         try:
968             os.makedirs(directory)
969         # pylint: disable-msg=W0704
970         except OSError:
971             pass
972
973
974 def exec_rsync(source, target):
975     """Simple wrapper around rsync to install files
976
977     @source: source file/directory
978     @target: target file/directory"""
979     logging.debug("Source: %s / Target: %s", source, target)
980     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
981     proc.wait()
982     if proc.returncode == 12:
983         logging.critical("Fatal: No space left on device")
984         cleanup()
985         sys.exit(1)
986
987     if proc.returncode != 0:
988         logging.critical("Fatal: could not install %s", source)
989         cleanup()
990         sys.exit(1)
991
992
993 def write_uuid(target_file):
994     """Generates an returns uuid and write it to the specified file
995
996     @target_file: filename to write the uuid to
997     """
998
999     fileh = open(target_file, "w")
1000     uid = str(uuid.uuid4())
1001     fileh.write(uid)
1002     fileh.close()
1003     return uid
1004
1005
1006 def get_uuid(target):
1007     """Get the uuid of the specified target. Will generate an uuid if none exist.
1008
1009     @target: directory/mountpoint containing the grml layout
1010     """
1011
1012     conf_target = target + "/conf/"
1013     uuid_file_name = conf_target + "/bootid.txt"
1014     if os.path.isdir(conf_target):
1015         if os.path.isfile(uuid_file_name):
1016             uuid_file = open(uuid_file_name, "r")
1017             uid = uuid_file.readline().strip()
1018             uuid_file.close()
1019             return uid
1020         else:
1021             return write_uuid(uuid_file_name)
1022     else:
1023         execute(mkdir, conf_target)
1024         return write_uuid(uuid_file_name)
1025
1026
1027 def get_shortname(grml_flavour):
1028     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
1029     @grml_flavour: flavour name which shold be translated to shortname"""
1030
1031     return re.sub(r"[,._-]", "", grml_flavour)
1032
1033
1034 def copy_system_files(grml_flavour, iso_mount, target):
1035     """copy grml's main files (like squashfs, kernel and initrd) to a given target
1036
1037     @grml_flavour: name of grml flavour the configuration should be generated for
1038     @iso_mount: path where a grml ISO is mounted on
1039     @target: path where grml's main files should be copied to"""
1040
1041     squashfs = search_file(grml_flavour + ".squashfs", iso_mount)
1042     if squashfs is None:
1043         logging.error("error locating squashfs file")
1044         raise CriticalException(
1045             "squashfs file not found, please check that your iso is not corrupt"
1046         )
1047     else:
1048         squashfs_target = target + "/live/" + grml_flavour + "/"
1049         execute(mkdir, squashfs_target)
1050     exec_rsync(squashfs, squashfs_target + grml_flavour + ".squashfs")
1051
1052     for prefix in grml_flavour + "/", "":
1053         filesystem_module = search_file(prefix + "filesystem.module", iso_mount)
1054         if filesystem_module:
1055             break
1056     if filesystem_module is None:
1057         logging.error("error locating filesystem.module file")
1058         raise CriticalException("filesystem.module not found")
1059     else:
1060         exec_rsync(filesystem_module, squashfs_target + "filesystem.module")
1061
1062     shortname = get_shortname(grml_flavour)
1063     if os.path.isdir(iso_mount + "/boot/" + shortname):
1064         exec_rsync(iso_mount + "/boot/" + shortname, target + "/boot")
1065     else:
1066         kernel = search_file("vmlinuz", iso_mount)
1067         if kernel is None:
1068             # compat for releases < 2011.12
1069             kernel = search_file("linux26", iso_mount)
1070
1071         if kernel is None:
1072             logging.error("error locating kernel file")
1073             raise CriticalException("Kernel not found")
1074
1075         source = os.path.dirname(kernel) + "/"
1076         dest = target + "/" + os.path.dirname(kernel).replace(iso_mount, "") + "/"
1077         execute(mkdir, dest)
1078         exec_rsync(source, dest)
1079
1080
1081 def copy_grml_files(grml_flavour, iso_mount, target):
1082     """copy some minor grml files to a given target
1083
1084     @grml_flavour: the current grml_flavour
1085     @iso_mount: path where a grml ISO is mounted on
1086     @target: path where grml's main files should be copied to"""
1087
1088     grml_target = target + "/grml/"
1089     execute(mkdir, grml_target)
1090
1091     grml_prefixe = ["GRML", "grml"]
1092     for prefix in grml_prefixe:
1093         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
1094         if os.path.exists(filename):
1095             exec_rsync(filename, grml_target)
1096             break
1097     else:
1098         logging.warning(
1099             "Warning: could not find flavour directory for %s ", grml_flavour
1100         )
1101
1102
1103 def copy_addons(iso_mount, target):
1104     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1105
1106     @iso_mount: path where a grml ISO is mounted on
1107     @target: path where grml's main files should be copied to"""
1108
1109     addons = target + "/boot/addons/"
1110     execute(mkdir, addons)
1111
1112     for addon_file in glob.glob(iso_mount + "/boot/addons/*"):
1113         filename = os.path.basename(addon_file)
1114         src_file = iso_mount + "/boot/addons/" + os.path.basename(addon_file)
1115         logging.debug("Copying addon file %s" % filename)
1116         exec_rsync(src_file, addons)
1117
1118
1119 def build_loopbackcfg(target):
1120     """Generate GRUB's loopback.cfg based on existing config files.
1121
1122     @target: target directory
1123     """
1124
1125     grub_dir = "/boot/grub/"
1126     mkdir(os.path.join(target, grub_dir))
1127
1128     f = open(target + grub_dir + "loopback.cfg", "w")
1129
1130     f.write("# grml2usb generated grub2 configuration file\n")
1131     f.write("source /boot/grub/header.cfg\n")
1132
1133     for defaults in glob.glob(
1134         target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"
1135     ):
1136         sourcefile = defaults.split(target + os.path.sep)[1]
1137         logging.debug("Found source file" + sourcefile)
1138         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1139
1140     for ops in glob.glob(
1141         target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"
1142     ):
1143         sourcefile = ops.split(target + os.path.sep)[1]
1144         logging.debug("Found source file" + sourcefile)
1145         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1146
1147     f.write("source /boot/grub/addons.cfg\n")
1148     f.write("source /boot/grub/footer.cfg\n")
1149     f.close()
1150
1151
1152 def glob_and_copy(filepattern, dst):
1153     """Glob on specified filepattern and copy the result to dst
1154
1155     @filepattern: globbing pattern
1156     @dst: target directory
1157     """
1158     for name in glob.glob(filepattern):
1159         copy_if_exist(name, dst)
1160
1161
1162 def search_and_copy(filename, search_path, dst):
1163     """Search for the specified filename at searchpath and copy it to dst
1164
1165     @filename: filename to look for
1166     @search_path: base search file
1167     @dst: destionation to copy the file to
1168     """
1169     file_location = search_file(filename, search_path)
1170     copy_if_exist(file_location, dst)
1171
1172
1173 def copy_if_exist(filename, dst):
1174     """Copy filename to dst if filename is set.
1175
1176     @filename: a filename
1177     @dst: dst file
1178     """
1179     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1180         exec_rsync(filename, dst)
1181
1182
1183 def copy_bootloader_files(iso_mount, target, grml_flavour):
1184     """Copy grml's bootloader files to a given target
1185
1186     @iso_mount: path where a grml ISO is mounted on
1187     @target: path where grml's main files should be copied to
1188     @grml_flavour: name of the current processed grml_flavour
1189     """
1190
1191     syslinux_target = target + "/boot/syslinux/"
1192     execute(mkdir, syslinux_target)
1193
1194     grub_target = target + "/boot/grub/"
1195     execute(mkdir, grub_target)
1196
1197     logo = search_file("logo.16", iso_mount, required=True)
1198     exec_rsync(logo, syslinux_target + "logo.16")
1199
1200     bootx64_efi = search_file("bootx64.efi", iso_mount)
1201     if bootx64_efi:
1202         mkdir(target + "/efi/boot/")
1203         exec_rsync(bootx64_efi, target + "/efi/boot/bootx64.efi")
1204
1205     efi_img = search_file("efi.img", iso_mount)
1206     if efi_img:
1207         mkdir(target + "/boot/")
1208         exec_rsync(efi_img, target + "/boot/efi.img")
1209         handle_secure_boot(target, efi_img)
1210
1211     execute(mkdir, target + "/conf/")
1212     glob_and_copy(iso_mount + "/conf/bootfile_*", target + "/conf/")
1213
1214     for ffile in ["f%d" % number for number in range(1, 11)]:
1215         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1216
1217     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1218     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1219     if os.path.isfile(syslinux_target + "ldlinux.sys"):
1220         os.unlink(syslinux_target + "ldlinux.sys")
1221
1222     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1223     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1224
1225     if not source_dir:
1226         raise CriticalException(
1227             "file default.cfg could not be found.\n"
1228             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1229             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version."
1230         )
1231
1232     if not os.path.exists(iso_mount + "/boot/grub/footer.cfg"):
1233         logging.warning(
1234             "Warning: Grml releases older than 2011.12 support only one flavour in grub."
1235         )
1236
1237     for expr in (
1238         name,
1239         "distri.cfg",
1240         defaults_file,
1241         "grml.png",
1242         "hd.cfg",
1243         "isolinux.cfg",
1244         "isolinux.bin",
1245         "isoprompt.cfg",
1246         "options.cfg",
1247         "prompt.cfg",
1248         "vesamenu.cfg",
1249         "grml.png",
1250         "*.c32",
1251     ):
1252         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1253
1254     for modules_dir in options.syslinuxlibs + SYSLINUX_LIBS:
1255         if os.path.isdir(modules_dir):
1256             for filename in glob.glob1(syslinux_target, "*.c32"):
1257                 copy_if_exist(os.path.join(modules_dir, filename), syslinux_target)
1258             break
1259
1260     # copy the addons_*.cfg file to the new syslinux directory
1261     glob_and_copy(iso_mount + source_dir + "addon*.cfg", syslinux_target)
1262
1263     search_and_copy(
1264         "hidden.cfg", iso_mount + source_dir, syslinux_target + "new_" + "hidden.cfg"
1265     )
1266
1267     # copy all grub files from ISO
1268     glob_and_copy(iso_mount + "/boot/grub/*", grub_target)
1269
1270     # finally (after all GRUB files have been installed) build static loopback.cfg
1271     build_loopbackcfg(target)
1272
1273
1274 def install_iso_files(grml_flavour, iso_mount, device, target):
1275     """Copy files from ISO to given target
1276
1277     @grml_flavour: name of grml flavour the configuration should be generated for
1278     @iso_mount: path where a grml ISO is mounted on
1279     @device: device/partition where bootloader should be installed to
1280     @target: path where grml's main files should be copied to"""
1281
1282     global GRML_DEFAULT
1283     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1284     if options.dryrun:
1285         return 0
1286     elif not options.bootloaderonly:
1287         logging.info("Copying files. This might take a while....")
1288         try:
1289             copy_system_files(grml_flavour, iso_mount, target)
1290             copy_grml_files(grml_flavour, iso_mount, target)
1291         except CriticalException as error:
1292             logging.critical("Execution failed: %s", error)
1293             sys.exit(1)
1294
1295     if not options.skipaddons:
1296         if not search_file("addons", iso_mount):
1297             logging.info("Could not find addons, therefore not installing.")
1298         else:
1299             copy_addons(iso_mount, target)
1300
1301     if not options.copyonly:
1302         copy_bootloader_files(iso_mount, target, grml_flavour)
1303
1304         if not options.dryrun:
1305             handle_bootloader_config(grml_flavour, device, target)
1306
1307     # make sure we sync filesystems before returning
1308     logging.info("Synching data (this might take a while)")
1309     proc = subprocess.Popen(["sync"])
1310     proc.wait()
1311
1312
1313 def get_device_from_partition(partition):
1314     device = partition
1315     partition_number = None
1316     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1317         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1318         if m:
1319             device = m.group(1)
1320             partition_number = int(m.group(2)) - 1
1321     return (device, partition_number)
1322
1323
1324 def get_flavour(flavour_str):
1325     """Returns the flavour of a grml version string
1326     """
1327     return re.match(r"[\w-]*", flavour_str).group()
1328
1329
1330 def identify_grml_flavour(mountpath):
1331     """Get name of grml flavour
1332
1333     @mountpath: path where the grml ISO is mounted to
1334     @return: name of grml-flavour"""
1335
1336     version_files = search_file("grml-version", mountpath, lst_return=True)
1337
1338     if not version_files:
1339         if mountpath.startswith("/run/live/medium"):
1340             logging.critical("Error: could not find grml-version file.")
1341             logging.critical(
1342                 "Looks like your system is running from RAM but required files are not available."
1343             )
1344             logging.critical(
1345                 "Please either boot without toram=... or use boot option toram instead of toram=..."
1346             )
1347         else:
1348             logging.critical("Error: could not find grml-version file.")
1349         cleanup()
1350         sys.exit(1)
1351
1352     flavours = []
1353     logging.debug("version_files = %s", version_files)
1354     for version_file in version_files:
1355         tmpfile = None
1356         try:
1357             tmpfile = open(version_file, "r")
1358             for line in tmpfile.readlines():
1359                 flavours.append(get_flavour(line))
1360         finally:
1361             if tmpfile:
1362                 tmpfile.close()
1363
1364     return flavours
1365
1366
1367 def get_bootoptions(grml_flavour):
1368     """Returns bootoptions for specific flavour
1369
1370     @grml_flavour: name of the grml_flavour
1371     """
1372     # do NOT write "None" in kernel cmdline
1373     if not options.bootoptions:
1374         bootopt = ""
1375     else:
1376         bootopt = " ".join(options.bootoptions)
1377     bootopt = bootopt.replace("%flavour", grml_flavour)
1378     return bootopt
1379
1380
1381 def handle_grub_config(grml_flavour, device, target):
1382     """Main handler for generating grub (v1 and v2) configuration
1383
1384     @grml_flavour: name of grml flavour the configuration should be generated for
1385     @device: device/partition where grub should be installed to
1386     @target: path of grub's configuration files"""
1387
1388     global UUID
1389
1390     logging.debug("Updating grub configuration")
1391
1392     grub_target = target + "/boot/grub/"
1393
1394     bootid_re = re.compile(r"bootid=[\w_-]+")
1395     live_media_path_re = re.compile(r"live-media-path=[\w_/-]+")
1396
1397     bootopt = get_bootoptions(grml_flavour)
1398
1399     remove_regexes = []
1400     option_re = re.compile(r"(.*/boot/.*(linux26|vmlinuz).*)")
1401
1402     if options.removeoption:
1403         for regex in options.removeoption:
1404             remove_regexes.append(re.compile(regex))
1405
1406     shortname = get_shortname(grml_flavour)
1407     for filename in glob.glob(grub_target + "*.cfg"):
1408         for line in fileinput.input(filename, inplace=1):
1409             line = line.rstrip("\r\n")
1410             if option_re.search(line):
1411                 line = bootid_re.sub("", line)
1412                 if shortname in filename:
1413                     line = live_media_path_re.sub("", line)
1414                     line = line.rstrip() + " live-media-path=/live/%s/ " % (
1415                         grml_flavour
1416                     )
1417                 if bootopt.strip():
1418                     line = line.replace(" {} ".format(bootopt.strip()), " ")
1419                     if line.endswith(bootopt):
1420                         line = line[: -len(bootopt)]
1421                 line = line.rstrip() + r" bootid=%s %s " % (UUID, bootopt)
1422                 for regex in remove_regexes:
1423                     line = regex.sub(" ", line)
1424             print(line)
1425         fileinput.close()
1426
1427
1428 def initial_syslinux_config(target):
1429     """Generates initial syslinux configuration
1430
1431     @target path of syslinux's configuration files"""
1432
1433     target = target + "/"
1434     filename = target + "grmlmain.cfg"
1435     if os.path.isfile(target + "grmlmain.cfg"):
1436         return
1437     data = open(filename, "w")
1438     data.write(generate_main_syslinux_config())
1439     data.close()
1440
1441     filename = target + "hiddens.cfg"
1442     data = open(filename, "w")
1443     data.write("include hidden.cfg\n")
1444     data.close()
1445
1446
1447 def add_entry_if_not_present(filename, entry):
1448     """Write entry into filename if entry is not already in the file
1449
1450     @filename: name of the file
1451     @entry: data to write to the file
1452     """
1453     data = open(filename, "a+")
1454     data.seek(0)
1455     for line in data:
1456         if line == entry:
1457             break
1458     else:
1459         data.write(entry)
1460
1461     data.close()
1462
1463
1464 def get_flavour_filename(flavour):
1465     """Generate a iso9960 save filename out of the specified flavour
1466
1467     @flavour: grml flavour
1468     """
1469     return flavour.replace("-", "_")
1470
1471
1472 def adjust_syslinux_bootoptions(src, flavour):
1473     """Adjust existing bootoptions of specified syslinux config to
1474     grml2usb specific ones, e.g. change the location of the kernel...
1475
1476     @src: config file to alter
1477     @flavour: grml flavour
1478     """
1479
1480     append_re = re.compile(r"^(\s*append.*/boot/.*)$", re.I)
1481     # flavour_re = re.compile("(label.*)(grml\w+)")
1482     default_re = re.compile(r"(default.cfg)")
1483     bootid_re = re.compile(r"bootid=[\w_-]+")
1484     live_media_path_re = re.compile(r"live-media-path=[\w_/-]+")
1485
1486     bootopt = get_bootoptions(flavour)
1487
1488     regexe = []
1489     option_re = None
1490     if options.removeoption:
1491         option_re = re.compile(r"/boot/.*/(initrd.gz|initrd.img)")
1492
1493         for regex in options.removeoption:
1494             regexe.append(re.compile(r"%s" % regex))
1495
1496     for line in fileinput.input(src, inplace=1):
1497         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1498         line = default_re.sub(r"%s-\1" % flavour, line)
1499         line = bootid_re.sub("", line)
1500         line = live_media_path_re.sub("", line)
1501         line = append_re.sub(r"\1 live-media-path=/live/%s/ " % flavour, line)
1502         line = append_re.sub(r"\1 boot=live %s " % bootopt, line)
1503         line = append_re.sub(r"\1 %s=%s " % ("bootid", UUID), line)
1504         if option_re and option_re.search(line):
1505             for regex in regexe:
1506                 line = regex.sub(" ", line)
1507         sys.stdout.write(line)
1508     fileinput.close()
1509
1510
1511 def adjust_labels(src, replacement):
1512     """Adjust the specified labels in the syslinux config file src with
1513     specified replacement
1514     """
1515     label_re = re.compile(r"^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1516     for line in fileinput.input(src, inplace=1):
1517         line = label_re.sub(replacement, line)
1518         sys.stdout.write(line)
1519     fileinput.close()
1520
1521
1522 def add_syslinux_entry(filename, grml_flavour):
1523     """Add includes for a specific grml_flavour to the specified filename
1524
1525     @filename: syslinux config file
1526     @grml_flavour: grml flavour to add
1527     """
1528
1529     entry_filename = "option_%s.cfg" % grml_flavour
1530     entry = "include %s\n" % entry_filename
1531
1532     add_entry_if_not_present(filename, entry)
1533     path = os.path.dirname(filename)
1534
1535     data = open(path + "/" + entry_filename, "w")
1536     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1537     data.close()
1538
1539
1540 def modify_filenames(grml_flavour, target, filenames):
1541     """Replace the standard filenames with the new ones
1542
1543     @grml_flavour: grml-flavour strin
1544     @target: directory where the files are located
1545     @filenames: list of filenames to alter
1546     """
1547     grml_filename = get_flavour_filename(grml_flavour)
1548     for filename in filenames:
1549         old_filename = "%s/%s" % (target, filename)
1550         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1551         os.rename(old_filename, new_filename)
1552
1553
1554 def remove_default_entry(filename):
1555     """Remove the default entry from specified syslinux file
1556
1557     @filename: syslinux config file
1558     """
1559     default_re = re.compile(r"^(\s*menu\s*default\s*)$", re.I)
1560     for line in fileinput.input(filename, inplace=1):
1561         if default_re.match(line):
1562             continue
1563         sys.stdout.write(line)
1564     fileinput.close()
1565
1566
1567 def handle_syslinux_config(grml_flavour, target):
1568     """Main handler for generating syslinux configuration
1569
1570     @grml_flavour: name of grml flavour the configuration should be generated for
1571     @target: path of syslinux's configuration files"""
1572
1573     logging.debug("Generating syslinux configuration")
1574     syslinux_target = target + "/boot/syslinux/"
1575     # should be present via  copy_bootloader_files(), but make sure it exits:
1576     execute(mkdir, syslinux_target)
1577     syslinux_cfg = syslinux_target + "syslinux.cfg"
1578
1579     # install main configuration only *once*, no matter how many ISOs we have:
1580     syslinux_config_file = open(syslinux_cfg, "w")
1581     syslinux_config_file.write("timeout 300\n")
1582     syslinux_config_file.write("include vesamenu.cfg\n")
1583     syslinux_config_file.close()
1584
1585     prompt_name = open(syslinux_target + "promptname.cfg", "w")
1586     prompt_name.write("menu label S^yslinux prompt\n")
1587     prompt_name.close()
1588
1589     initial_syslinux_config(syslinux_target)
1590     flavour_filename = get_flavour_filename(grml_flavour)
1591
1592     if search_file("default.cfg", syslinux_target):
1593         modify_filenames(grml_flavour, syslinux_target, ["grml.cfg", "default.cfg"])
1594
1595     filename = search_file("new_hidden.cfg", syslinux_target)
1596
1597     # process hidden file
1598     if not search_file("hidden.cfg", syslinux_target):
1599         new_hidden = syslinux_target + "hidden.cfg"
1600         os.rename(filename, new_hidden)
1601         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1602     else:
1603         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1604         os.rename(filename, new_hidden_file)
1605         adjust_labels(new_hidden_file, r"\1 %s-\2" % grml_flavour)
1606         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1607         entry = "include %s_hidden.cfg\n" % flavour_filename
1608         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1609
1610     new_default = "%s_default.cfg" % (flavour_filename)
1611     entry = "include %s\n" % new_default
1612     defaults_file = "%s/defaults.cfg" % syslinux_target
1613     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1614     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1615
1616     if os.path.isfile(defaults_file):
1617
1618         # remove default menu entry in menu
1619         remove_default_entry(new_default_with_path)
1620
1621         # adjust all labels for additional isos
1622         adjust_labels(new_default_with_path, r"\1 %s" % grml_flavour)
1623         adjust_labels(new_grml_cfg, r"\1 %s-\2" % grml_flavour)
1624
1625     # always adjust bootoptions
1626     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1627     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1628
1629     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1630
1631     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1632
1633
1634 def handle_secure_boot(target, efi_img):
1635     """Provide secure boot support by extracting files from /boot/efi.img
1636
1637     @target: path where grml's main files should be copied to
1638     @efi_img: path to the efi.img file that includes the files for secure boot
1639     """
1640
1641     mkdir(target + "/efi/boot/")
1642     efi_mountpoint = tempfile.mkdtemp(
1643         prefix="grml2usb", dir=os.path.abspath(options.tmpdir)
1644     )
1645     logging.debug("efi_mountpoint = %s" % efi_mountpoint)
1646     register_tmpfile(efi_mountpoint)
1647
1648     try:
1649         logging.debug(
1650             "mount(%s, %s, ['-o', 'ro', '-t', 'vfat']" % (efi_img, efi_mountpoint)
1651         )
1652         mount(efi_img, efi_mountpoint, ["-o", "ro", "-t", "vfat"])
1653     except CriticalException as error:
1654         logging.critical("Fatal: %s", error)
1655         sys.exit(1)
1656
1657     grub_cfg = search_file("grub.cfg", efi_mountpoint + "/boot/grub/")
1658     logging.debug("grub_cfg = %s" % grub_cfg)
1659     if not grub_cfg:
1660         logging.info(
1661             "No /boot/grub/grub.cfg found inside EFI image, looks like Secure Boot support is missing."
1662         )
1663     else:
1664         mkdir(target + "/boot/grub/x86_64-efi/")
1665         logging.debug(
1666             "exec_rsync(%s, %s + '/boot/grub/x86_64-efi/grub.cfg')" % (grub_cfg, target)
1667         )
1668         exec_rsync(grub_cfg, target + "/boot/grub/x86_64-efi/grub.cfg")
1669
1670         logging.debug(
1671             "exec_rsync(%s + '/EFI/BOOT/grubx64.efi', %s + '/efi/boot/grubx64.efi')'"
1672             % (efi_mountpoint, target)
1673         )
1674         exec_rsync(
1675             efi_mountpoint + "/EFI/BOOT/grubx64.efi", target + "/efi/boot/grubx64.efi"
1676         )
1677
1678         # NOTE - we're overwriting /efi/boot/bootx64.efi from copy_bootloader_files here
1679         logging.debug(
1680             "exec_rsync(%s + '/EFI/BOOT/bootx64.efi', %s + '/efi/boot/bootx64.efi')'"
1681             % (efi_mountpoint, target)
1682         )
1683         exec_rsync(
1684             efi_mountpoint + "/EFI/BOOT/bootx64.efi", target + "/efi/boot/bootx64.efi"
1685         )
1686
1687     try:
1688         unmount(efi_mountpoint, "")
1689         logging.debug("Unmounted %s" % efi_mountpoint)
1690         os.rmdir(efi_mountpoint)
1691         logging.debug("Removed directory %s" % efi_mountpoint)
1692     except Exception:
1693         logging.critical("RuntimeError while umount %s" % efi_mountpoint)
1694         sys.exit(1)
1695
1696
1697 def handle_bootloader_config(grml_flavour, device, target):
1698     """Main handler for generating bootloader's configuration
1699
1700     @grml_flavour: name of grml flavour the configuration should be generated for
1701     @device: device/partition where bootloader should be installed to
1702     @target: path of bootloader's configuration files"""
1703
1704     global UUID
1705     UUID = get_uuid(target)
1706     if options.skipsyslinuxconfig:
1707         logging.info("Skipping generation of syslinux configuration as requested.")
1708     else:
1709         try:
1710             handle_syslinux_config(grml_flavour, target)
1711         except CriticalException as error:
1712             logging.critical("Fatal: %s", error)
1713             sys.exit(1)
1714
1715     if options.skipgrubconfig:
1716         logging.info("Skipping generation of grub configuration as requested.")
1717     else:
1718         try:
1719             handle_grub_config(grml_flavour, device, target)
1720         except CriticalException as error:
1721             logging.critical("Fatal: %s", error)
1722             sys.exit(1)
1723
1724
1725 def install(image, device):
1726     """Install a grml image to the specified device
1727
1728     @image: directory or is file
1729     @device: partition or directory to install the device
1730     """
1731     iso_mountpoint = image
1732     remove_image_mountpoint = False
1733     if os.path.isdir(image):
1734         if options.force or os.path.exists(os.path.join(image, "live")):
1735             logging.info("Using %s as install base", image)
1736         else:
1737             q = input(
1738                 "%s does not look like a Grml system. "
1739                 "Do you really want to use this image? y/N " % image
1740             )
1741             if q.lower() == "y":
1742                 logging.info("Using %s as install base", image)
1743             else:
1744                 logging.info("Skipping install base %s", image)
1745     else:
1746         logging.info("Using ISO %s", image)
1747         iso_mountpoint = tempfile.mkdtemp(
1748             prefix="grml2usb", dir=os.path.abspath(options.tmpdir)
1749         )
1750         register_tmpfile(iso_mountpoint)
1751         remove_image_mountpoint = True
1752         try:
1753             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1754         except CriticalException as error:
1755             logging.critical("Fatal: %s", error)
1756             sys.exit(1)
1757
1758     try:
1759         install_grml(iso_mountpoint, device)
1760     finally:
1761         if remove_image_mountpoint:
1762             try:
1763                 remove_mountpoint(iso_mountpoint)
1764             except CriticalException:
1765                 cleanup()
1766                 raise
1767
1768
1769 def install_grml(mountpoint, device):
1770     """Main logic for copying files of the currently running Grml system.
1771
1772     @mountpoint: directory where currently running live system resides (usually /run/live/medium)
1773     @device: partition where the specified ISO should be installed to"""
1774
1775     device_mountpoint = device
1776     if os.path.isdir(device):
1777         logging.info("Specified device is a directory, therefore not mounting.")
1778         remove_device_mountpoint = False
1779     else:
1780         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1781         register_tmpfile(device_mountpoint)
1782         remove_device_mountpoint = True
1783         try:
1784             set_rw(device)
1785             mount(device, device_mountpoint, ["-o", "utf8,iocharset=iso8859-1"])
1786         except CriticalException:
1787             mount(device, device_mountpoint, "")
1788     try:
1789         grml_flavours = identify_grml_flavour(mountpoint)
1790         for flavour in set(grml_flavours):
1791             if not flavour:
1792                 logging.warning("No valid flavour found, please check your iso")
1793             logging.info('Identified grml flavour "%s".', flavour)
1794             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1795             GRML_FLAVOURS.add(flavour)
1796     finally:
1797         if remove_device_mountpoint:
1798             remove_mountpoint(device_mountpoint)
1799
1800
1801 def remove_mountpoint(mountpoint):
1802     """remove a registered mountpoint
1803     """
1804
1805     try:
1806         unmount(mountpoint, "")
1807         if os.path.isdir(mountpoint):
1808             os.rmdir(mountpoint)
1809             unregister_tmpfile(mountpoint)
1810     except CriticalException:
1811         cleanup()
1812         raise
1813
1814
1815 def handle_mbr(device):
1816     """Main handler for installing master boot record (MBR)
1817
1818     @device: device where the MBR should be installed to"""
1819
1820     if options.dryrun:
1821         logging.info("Would install MBR")
1822         return 0
1823
1824     mbr_device, partition_number = get_device_from_partition(device)
1825     if partition_number is None:
1826         logging.warning("Could not detect partition number, not activating partition")
1827
1828     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1829     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1830     if mbr_device == "/dev/loop":
1831         mbr_device = device
1832         logging.info(
1833             "Detected loop device - using %s as MBR device therefore", mbr_device
1834         )
1835
1836     mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1837     if options.syslinuxmbr:
1838         mbrcode = ""
1839         mbr_locations = (
1840             "/usr/lib/syslinux/mbr.bin",
1841             "/usr/lib/syslinux/bios/mbr.bin",
1842             "/usr/share/syslinux/mbr.bin",
1843         )
1844         for mbrpath in mbr_locations:
1845             if os.path.isfile(mbrpath):
1846                 mbrcode = mbrpath
1847                 break
1848
1849         if not mbrcode:
1850             str_locations = " or ".join(['"%s"' % x for x in mbr_locations])
1851             logging.error("Cannot find syslinux MBR, install it at %s)", str_locations)
1852             raise CriticalException(
1853                 "syslinux MBR  can not be found at %s." % str_locations
1854             )
1855     elif options.mbrmenu:
1856         mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1857
1858     try:
1859         install_mbr(mbrcode, mbr_device, partition_number, True)
1860     except (IOError, Exception) as error:
1861         logging.critical("Execution failed: %s", error)
1862         sys.exit(1)
1863
1864
1865 def handle_vfat(device):
1866     """Check for FAT specific settings and options
1867
1868     @device: device that should checked / formated"""
1869
1870     # make sure we have mkfs.vfat available
1871     if options.fat16:
1872         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1873             logging.critical("Sorry, mkfs.vfat not available. Exiting.")
1874             logging.critical("Please make sure to install dosfstools.")
1875             sys.exit(1)
1876
1877         if options.force:
1878             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1879         else:
1880             # make sure the user is aware of what he is doing
1881             f = input(
1882                 "Are you sure you want to format the specified partition with fat16? y/N "
1883             )
1884             if f == "y" or f == "Y":
1885                 logging.info(
1886                     "Note: you can skip this question using the option --force"
1887                 )
1888             else:
1889                 sys.exit(1)
1890         try:
1891             mkfs_fat16(device)
1892         except CriticalException as error:
1893             logging.critical("Execution failed: %s", error)
1894             sys.exit(1)
1895
1896     # check for vfat filesystem
1897     if device is not None and not os.path.isdir(device) and options.syslinux:
1898         try:
1899             check_for_fat(device)
1900         except CriticalException as error:
1901             logging.critical("Execution failed: %s", error)
1902             sys.exit(1)
1903
1904     if options.skipusbcheck:
1905         logging.info(
1906             "Not checking for removable USB device as requested via option --skip-usb-check."
1907         )
1908         return
1909
1910     if (
1911         not os.path.isdir(device)
1912         and not check_for_usbdevice(device)
1913         and not options.force
1914     ):
1915         print(
1916             "Warning: the specified device %s does not look like a removable usb device."
1917             % device
1918         )
1919         f = input("Do you really want to continue? y/N ")
1920         if f.lower() != "y":
1921             sys.exit(1)
1922
1923
1924 def handle_compat_warning(device):
1925     """Backwards compatible checks
1926
1927     @device: device that should be checked"""
1928
1929     # make sure we can replace old grml2usb script and warn user when using old way of life:
1930     if (
1931         device.startswith("/mnt/external")
1932         or device.startswith("/mnt/usb")
1933         and not options.force
1934     ):
1935         print("Warning: the semantics of grml2usb has changed.")
1936         print("Instead of using grml2usb /path/to/iso %s you might" % device)
1937         print("want to use grml2usb /path/to/iso /dev/... instead.")
1938         print("Please check out the grml2usb manpage for details.")
1939         f = input("Do you really want to continue? y/N ")
1940         if f.lower() != "y":
1941             sys.exit(1)
1942
1943
1944 def handle_logging():
1945     """Log handling and configuration"""
1946
1947     if options.verbose and options.quiet:
1948         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1949
1950     FORMAT = "%(message)s"
1951     if options.verbose:
1952         FORMAT = "%(asctime)-15s %(message)s"
1953         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1954     elif options.quiet:
1955         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1956     else:
1957         logging.basicConfig(level=logging.INFO, format=FORMAT)
1958
1959
1960 def handle_bootloader(device):
1961     """wrapper for installing bootloader
1962
1963     @device: device where bootloader should be installed to"""
1964
1965     # Install bootloader only if not using the --copy-only option
1966     if options.copyonly:
1967         logging.info(
1968             "Not installing bootloader and its files as requested via option copyonly."
1969         )
1970     elif os.path.isdir(device):
1971         logging.info("Not installing bootloader as %s is a directory.", device)
1972     else:
1973         install_bootloader(device)
1974
1975
1976 def check_options(opts):
1977     """Check compatibility of provided user opts
1978
1979     @opts option dict from OptionParser
1980     """
1981     if opts.grubmbr and not opts.grub:
1982         raise CriticalException("--grub-mbr requires --grub option.")
1983
1984     if opts.copyonly and opts.grub:
1985         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
1986
1987
1988 def check_programs():
1989     """check if all needed programs are installed"""
1990     if options.grub:
1991         global GRUB_INSTALL
1992         GRUB_INSTALL = which("grub-install") or which("grub2-install")
1993         if not GRUB_INSTALL:
1994             logging.critical(
1995                 "Fatal: grub-install not available (please install the "
1996                 "grub package or drop the --grub option)"
1997             )
1998             sys.exit(1)
1999
2000     if options.syslinux:
2001         if not which("syslinux"):
2002             logging.critical(
2003                 "Fatal: syslinux not available (please install the "
2004                 "syslinux package or use the --grub option)"
2005             )
2006             sys.exit(1)
2007
2008     if not which("rsync"):
2009         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2010         sys.exit(1)
2011
2012
2013 def load_loop():
2014     """Runs modprobe loop and throws away its output"""
2015     if not which("modprobe"):
2016         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
2017         logging.critical("Hint: is /sbin missing in PATH?")
2018         sys.exit(1)
2019
2020     proc = subprocess.Popen(
2021         ["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2022     )
2023     proc.communicate()
2024
2025
2026 def main():
2027     """Main invocation"""
2028
2029     try:
2030         if options.version:
2031             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
2032             sys.exit(0)
2033
2034         if len(args) < 2:
2035             parser.error("invalid usage")
2036
2037         # log handling
2038         handle_logging()
2039
2040         # make sure we have the appropriate permissions
2041         check_uid_root()
2042
2043         check_options(options)
2044
2045         load_loop()
2046
2047         logging.info("Executing grml2usb version %s", PROG_VERSION)
2048
2049         if options.dryrun:
2050             logging.info("Running in simulation mode as requested via option dry-run.")
2051
2052         check_programs()
2053
2054         # specified arguments
2055         device = os.path.realpath(args[len(args) - 1])
2056         isos = args[0 : len(args) - 1]
2057
2058         if not os.path.isdir(device):
2059             if device[-1:].isdigit():
2060                 if int(device[-1:]) > 4 or device[-2:].isdigit():
2061                     logging.warning(
2062                         "Warning: installing on partition number >4, booting *might* fail depending on your system."
2063                     )
2064
2065         # provide upgrade path
2066         handle_compat_warning(device)
2067
2068         if not options.skipbootflag:
2069             check_boot_flag(device)
2070
2071         # check for vfat partition
2072         handle_vfat(device)
2073
2074         # main operation (like installing files)
2075         for iso in isos:
2076             install(iso, device)
2077
2078         # install mbr
2079         is_superfloppy = not device[-1:].isdigit()
2080         if is_superfloppy:
2081             logging.info("Detected superfloppy format - not installing MBR")
2082
2083         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
2084             handle_mbr(device)
2085
2086         handle_bootloader(device)
2087
2088         logging.info(
2089             "Note: grml flavour %s was installed as the default booting system.",
2090             GRML_DEFAULT,
2091         )
2092
2093         for flavour in GRML_FLAVOURS:
2094             logging.info(
2095                 "Note: you can boot flavour %s using '%s' on the commandline.",
2096                 flavour,
2097                 flavour,
2098             )
2099
2100         # finally be polite :)
2101         logging.info(
2102             "Finished execution of grml2usb (%s). Have fun with your Grml system.",
2103             PROG_VERSION,
2104         )
2105
2106     except Exception as error:
2107         logging.critical("Fatal: %s", str(error))
2108         if options.verbose:
2109             logging.exception("Exception:")
2110         sys.exit(1)
2111
2112
2113 if __name__ == "__main__":
2114     try:
2115         main()
2116     except KeyboardInterrupt:
2117         logging.info("Received KeyboardInterrupt")
2118         cleanup()
2119
2120 # END OF FILE ##################################################################
2121 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8