Github action: do not install virtualenv + python3-setuptools
[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, _value, 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, _value, 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,fsync",
810         tmpf.name,
811         device,
812     )
813     proc = subprocess.Popen(
814         [
815             "dd",
816             "if=%s" % tmpf.name,
817             "of=%s" % device,
818             "bs=512",
819             "count=1",
820             "conv=notrunc,fsync",
821         ],
822         stderr=open(os.devnull, "r+"),
823     )
824     proc.wait()
825     if proc.returncode != 0:
826         raise Exception("error executing dd (third run)")
827     del tmpf
828
829     logging.debug("Probing device via 'blockdev --rereadpt %s'", device)
830     proc = subprocess.Popen(["blockdev", "--rereadpt", device])
831     proc.wait()
832     if proc.returncode != 0:
833         raise Exception(
834             "Couldn't execute blockdev on '%s' (install util-linux?)", device
835         )
836
837     set_rw(device)
838
839
840 def mount(source, target, mount_options):
841     """Mount specified source on given target
842
843     @source: name of device/ISO that should be mounted
844     @target: directory where the ISO should be mounted to
845     @options: mount specific options"""
846
847     # note: options.dryrun does not work here, as we have to
848     # locate files and identify the grml flavour
849
850     for x in open("/proc/mounts", "r").readlines():
851         if x.startswith(source):
852             raise CriticalException(
853                 (
854                     "Error executing mount: {0} already mounted - "
855                     "please unmount before invoking grml2usb"
856                 ).format(source)
857             )
858
859     if os.path.isdir(source):
860         logging.debug("Source %s is not a device, therefore not mounting.", source)
861         return 0
862
863     logging.debug("mount %s %s %s", mount_options, source, target)
864     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
865     proc.wait()
866     if proc.returncode != 0:
867         raise CriticalException(
868             "Error executing mount (no filesystem on the partition?)"
869         )
870     else:
871         logging.debug("register_mountpoint(%s)", target)
872         register_mountpoint(target)
873
874
875 def unmount(target, unmount_options):
876     """Unmount specified target
877
878     @target: target where something is mounted on and which should be unmounted
879     @options: options for umount command"""
880
881     # make sure we unmount only already mounted targets
882     target_unmount = False
883     mounts = open("/proc/mounts").readlines()
884     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
885     for line in mounts:
886         if re.match(mountstring, line):
887             target_unmount = True
888
889     if not target_unmount:
890         logging.debug("%s not mounted anymore", target)
891     else:
892         logging.debug("umount %s %s", list(unmount_options), target)
893         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
894         proc.wait()
895         if proc.returncode != 0:
896             raise Exception("Error executing umount")
897         else:
898             logging.debug("unregister_mountpoint(%s)", target)
899             unregister_mountpoint(target)
900
901
902 def extract_device_name(device):
903     """Extract the device name of a given path
904
905     @device: device name, like /dev/sda1 or /dev/sda
906     """
907     return re.match(r"/dev/(.*?)\d*$", device).group(1)
908
909
910 def check_for_usbdevice(device):
911     """Check whether the specified device is a removable USB device
912
913     @device: device name, like /dev/sda1 or /dev/sda
914     """
915
916     usbdevice = extract_device_name(device)
917     # newer systems:
918     usbdev = os.path.realpath("/sys/class/block/" + usbdevice + "/removable")
919     if not os.path.isfile(usbdev):
920         # Ubuntu with kernel 2.6.24 for example:
921         usbdev = os.path.realpath("/sys/block/" + usbdevice + "/removable")
922
923     if os.path.isfile(usbdev):
924         is_usb = open(usbdev).readline()
925         if is_usb.find("1"):
926             return 0
927
928     return 1
929
930
931 def check_for_fat(partition):
932     """Check whether specified partition is a valid VFAT/FAT16 filesystem
933
934     @partition: device name of partition"""
935
936     if not os.access(partition, os.R_OK):
937         raise CriticalException(
938             "Failed to read device %s"
939             " (wrong UID/permissions or device/directory not present?)" % partition
940         )
941
942     try:
943         filesystem = (
944             subprocess.check_output(
945                 ["/sbin/blkid", "-s", "TYPE", "-o", "value", partition]
946             )
947             .decode()
948             .rstrip()
949         )
950
951         if filesystem != "vfat":
952             raise CriticalException(
953                 "Partition %s does not contain a FAT16 filesystem. "
954                 "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition)
955             )
956
957     except OSError:
958         raise CriticalException(
959             "Sorry, /sbin/blkid not available (install util-linux?)"
960         )
961
962
963 def mkdir(directory):
964     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
965
966     # just silently pass as it's just fine it the directory exists
967     if not os.path.isdir(directory):
968         try:
969             os.makedirs(directory)
970         # pylint: disable-msg=W0704
971         except OSError:
972             pass
973
974
975 def exec_rsync(source, target):
976     """Simple wrapper around rsync to install files
977
978     @source: source file/directory
979     @target: target file/directory"""
980     logging.debug("Source: %s / Target: %s", source, target)
981     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
982     proc.wait()
983     if proc.returncode == 12:
984         logging.critical("Fatal: No space left on device")
985         cleanup()
986         sys.exit(1)
987
988     if proc.returncode != 0:
989         logging.critical("Fatal: could not install %s", source)
990         cleanup()
991         sys.exit(1)
992
993
994 def write_uuid(target_file):
995     """Generates an returns uuid and write it to the specified file
996
997     @target_file: filename to write the uuid to
998     """
999
1000     fileh = open(target_file, "w")
1001     uid = str(uuid.uuid4())
1002     fileh.write(uid)
1003     fileh.close()
1004     return uid
1005
1006
1007 def get_uuid(target):
1008     """Get the uuid of the specified target. Will generate an uuid if none exist.
1009
1010     @target: directory/mountpoint containing the grml layout
1011     """
1012
1013     conf_target = target + "/conf/"
1014     uuid_file_name = conf_target + "/bootid.txt"
1015     if os.path.isdir(conf_target):
1016         if os.path.isfile(uuid_file_name):
1017             uuid_file = open(uuid_file_name, "r")
1018             uid = uuid_file.readline().strip()
1019             uuid_file.close()
1020             return uid
1021         else:
1022             return write_uuid(uuid_file_name)
1023     else:
1024         execute(mkdir, conf_target)
1025         return write_uuid(uuid_file_name)
1026
1027
1028 def get_shortname(grml_flavour):
1029     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
1030     @grml_flavour: flavour name which shold be translated to shortname"""
1031
1032     return re.sub(r"[,._-]", "", grml_flavour)
1033
1034
1035 def copy_system_files(grml_flavour, iso_mount, target):
1036     """copy grml's main files (like squashfs, kernel and initrd) to a given target
1037
1038     @grml_flavour: name of grml flavour the configuration should be generated for
1039     @iso_mount: path where a grml ISO is mounted on
1040     @target: path where grml's main files should be copied to"""
1041
1042     squashfs = search_file(grml_flavour + ".squashfs", iso_mount)
1043     if squashfs is None:
1044         logging.error("error locating squashfs file")
1045         raise CriticalException(
1046             "squashfs file not found, please check that your iso is not corrupt"
1047         )
1048     else:
1049         squashfs_target = target + "/live/" + grml_flavour + "/"
1050         execute(mkdir, squashfs_target)
1051     exec_rsync(squashfs, squashfs_target + grml_flavour + ".squashfs")
1052
1053     for prefix in grml_flavour + "/", "":
1054         filesystem_module = search_file(prefix + "filesystem.module", iso_mount)
1055         if filesystem_module:
1056             break
1057     if filesystem_module is None:
1058         logging.error("error locating filesystem.module file")
1059         raise CriticalException("filesystem.module not found")
1060     else:
1061         exec_rsync(filesystem_module, squashfs_target + "filesystem.module")
1062
1063     shortname = get_shortname(grml_flavour)
1064     if os.path.isdir(iso_mount + "/boot/" + shortname):
1065         exec_rsync(iso_mount + "/boot/" + shortname, target + "/boot")
1066     else:
1067         kernel = search_file("vmlinuz", iso_mount)
1068         if kernel is None:
1069             # compat for releases < 2011.12
1070             kernel = search_file("linux26", iso_mount)
1071
1072         if kernel is None:
1073             logging.error("error locating kernel file")
1074             raise CriticalException("Kernel not found")
1075
1076         source = os.path.dirname(kernel) + "/"
1077         dest = target + "/" + os.path.dirname(kernel).replace(iso_mount, "") + "/"
1078         execute(mkdir, dest)
1079         exec_rsync(source, dest)
1080
1081
1082 def copy_grml_files(grml_flavour, iso_mount, target):
1083     """copy some minor grml files to a given target
1084
1085     @grml_flavour: the current grml_flavour
1086     @iso_mount: path where a grml ISO is mounted on
1087     @target: path where grml's main files should be copied to"""
1088
1089     grml_target = target + "/grml/"
1090     execute(mkdir, grml_target)
1091
1092     grml_prefixe = ["GRML", "grml"]
1093     for prefix in grml_prefixe:
1094         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
1095         if os.path.exists(filename):
1096             exec_rsync(filename, grml_target)
1097             break
1098     else:
1099         logging.warning(
1100             "Warning: could not find flavour directory for %s ", grml_flavour
1101         )
1102
1103
1104 def copy_addons(iso_mount, target):
1105     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1106
1107     @iso_mount: path where a grml ISO is mounted on
1108     @target: path where grml's main files should be copied to"""
1109
1110     addons = target + "/boot/addons/"
1111     execute(mkdir, addons)
1112
1113     for addon_file in glob.glob(iso_mount + "/boot/addons/*"):
1114         filename = os.path.basename(addon_file)
1115         src_file = iso_mount + "/boot/addons/" + os.path.basename(addon_file)
1116         logging.debug("Copying addon file %s" % filename)
1117         exec_rsync(src_file, addons)
1118
1119
1120 def build_loopbackcfg(target):
1121     """Generate GRUB's loopback.cfg based on existing config files.
1122
1123     @target: target directory
1124     """
1125
1126     grub_dir = "/boot/grub/"
1127     mkdir(os.path.join(target, grub_dir))
1128
1129     f = open(target + grub_dir + "loopback.cfg", "w")
1130
1131     f.write("# grml2usb generated grub2 configuration file\n")
1132     f.write("source /boot/grub/header.cfg\n")
1133
1134     for defaults in glob.glob(
1135         target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"
1136     ):
1137         sourcefile = defaults.split(target + os.path.sep)[1]
1138         logging.debug("Found source file" + sourcefile)
1139         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1140
1141     for ops in glob.glob(
1142         target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"
1143     ):
1144         sourcefile = ops.split(target + os.path.sep)[1]
1145         logging.debug("Found source file" + sourcefile)
1146         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1147
1148     f.write("source /boot/grub/addons.cfg\n")
1149     f.write("source /boot/grub/footer.cfg\n")
1150     f.close()
1151
1152
1153 def glob_and_copy(filepattern, dst):
1154     """Glob on specified filepattern and copy the result to dst
1155
1156     @filepattern: globbing pattern
1157     @dst: target directory
1158     """
1159     for name in glob.glob(filepattern):
1160         copy_if_exist(name, dst)
1161
1162
1163 def search_and_copy(filename, search_path, dst):
1164     """Search for the specified filename at searchpath and copy it to dst
1165
1166     @filename: filename to look for
1167     @search_path: base search file
1168     @dst: destionation to copy the file to
1169     """
1170     file_location = search_file(filename, search_path)
1171     copy_if_exist(file_location, dst)
1172
1173
1174 def copy_if_exist(filename, dst):
1175     """Copy filename to dst if filename is set.
1176
1177     @filename: a filename
1178     @dst: dst file
1179     """
1180     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1181         exec_rsync(filename, dst)
1182
1183
1184 def copy_bootloader_files(iso_mount, target, grml_flavour):
1185     """Copy grml's bootloader files to a given target
1186
1187     @iso_mount: path where a grml ISO is mounted on
1188     @target: path where grml's main files should be copied to
1189     @grml_flavour: name of the current processed grml_flavour
1190     """
1191
1192     syslinux_target = target + "/boot/syslinux/"
1193     execute(mkdir, syslinux_target)
1194
1195     grub_target = target + "/boot/grub/"
1196     execute(mkdir, grub_target)
1197
1198     logo = search_file("logo.16", iso_mount, required=True)
1199     exec_rsync(logo, syslinux_target + "logo.16")
1200
1201     bootx64_efi = search_file("bootx64.efi", iso_mount)
1202     if bootx64_efi:
1203         mkdir(target + "/efi/boot/")
1204         exec_rsync(bootx64_efi, target + "/efi/boot/bootx64.efi")
1205
1206     efi_img = search_file("efi.img", iso_mount)
1207     if efi_img:
1208         mkdir(target + "/boot/")
1209         exec_rsync(efi_img, target + "/boot/efi.img")
1210         handle_secure_boot(target, efi_img)
1211
1212     execute(mkdir, target + "/conf/")
1213     glob_and_copy(iso_mount + "/conf/bootfile_*", target + "/conf/")
1214
1215     for ffile in ["f%d" % number for number in range(1, 11)]:
1216         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1217
1218     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1219     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1220     if os.path.isfile(syslinux_target + "ldlinux.sys"):
1221         os.unlink(syslinux_target + "ldlinux.sys")
1222
1223     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1224     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1225
1226     if not source_dir:
1227         raise CriticalException(
1228             "file default.cfg could not be found.\n"
1229             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1230             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version."
1231         )
1232
1233     if not os.path.exists(iso_mount + "/boot/grub/footer.cfg"):
1234         logging.warning(
1235             "Warning: Grml releases older than 2011.12 support only one flavour in grub."
1236         )
1237
1238     for expr in (
1239         name,
1240         "distri.cfg",
1241         defaults_file,
1242         "grml.png",
1243         "hd.cfg",
1244         "isolinux.cfg",
1245         "isolinux.bin",
1246         "isoprompt.cfg",
1247         "options.cfg",
1248         "prompt.cfg",
1249         "vesamenu.cfg",
1250         "grml.png",
1251         "*.c32",
1252     ):
1253         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1254
1255     for modules_dir in options.syslinuxlibs + SYSLINUX_LIBS:
1256         if os.path.isdir(modules_dir):
1257             for filename in glob.glob1(syslinux_target, "*.c32"):
1258                 copy_if_exist(os.path.join(modules_dir, filename), syslinux_target)
1259             break
1260
1261     # copy the addons_*.cfg file to the new syslinux directory
1262     glob_and_copy(iso_mount + source_dir + "addon*.cfg", syslinux_target)
1263
1264     search_and_copy(
1265         "hidden.cfg", iso_mount + source_dir, syslinux_target + "new_" + "hidden.cfg"
1266     )
1267
1268     # copy all grub files from ISO
1269     glob_and_copy(iso_mount + "/boot/grub/*", grub_target)
1270
1271     # finally (after all GRUB files have been installed) build static loopback.cfg
1272     build_loopbackcfg(target)
1273
1274
1275 def install_iso_files(grml_flavour, iso_mount, device, target):
1276     """Copy files from ISO to given target
1277
1278     @grml_flavour: name of grml flavour the configuration should be generated for
1279     @iso_mount: path where a grml ISO is mounted on
1280     @device: device/partition where bootloader should be installed to
1281     @target: path where grml's main files should be copied to"""
1282
1283     global GRML_DEFAULT
1284     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1285     if options.dryrun:
1286         return 0
1287     elif not options.bootloaderonly:
1288         logging.info("Copying files. This might take a while....")
1289         try:
1290             copy_system_files(grml_flavour, iso_mount, target)
1291             copy_grml_files(grml_flavour, iso_mount, target)
1292         except CriticalException as error:
1293             logging.critical("Execution failed: %s", error)
1294             sys.exit(1)
1295
1296     if not options.skipaddons:
1297         if not search_file("addons", iso_mount):
1298             logging.info("Could not find addons, therefore not installing.")
1299         else:
1300             copy_addons(iso_mount, target)
1301
1302     if not options.copyonly:
1303         copy_bootloader_files(iso_mount, target, grml_flavour)
1304
1305         if not options.dryrun:
1306             handle_bootloader_config(grml_flavour, device, target)
1307
1308     # make sure we sync filesystems before returning
1309     logging.info("Synching data (this might take a while)")
1310     proc = subprocess.Popen(["sync"])
1311     proc.wait()
1312
1313
1314 def get_device_from_partition(partition):
1315     device = partition
1316     partition_number = None
1317     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1318         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1319         if m:
1320             device = m.group(1)
1321             partition_number = int(m.group(2)) - 1
1322     return (device, partition_number)
1323
1324
1325 def get_flavour(flavour_str):
1326     """Returns the flavour of a grml version string"""
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     try:
1805         unmount(mountpoint, "")
1806         if os.path.isdir(mountpoint):
1807             os.rmdir(mountpoint)
1808             unregister_tmpfile(mountpoint)
1809     except CriticalException:
1810         cleanup()
1811         raise
1812
1813
1814 def handle_mbr(device):
1815     """Main handler for installing master boot record (MBR)
1816
1817     @device: device where the MBR should be installed to"""
1818
1819     if options.dryrun:
1820         logging.info("Would install MBR")
1821         return 0
1822
1823     mbr_device, partition_number = get_device_from_partition(device)
1824     if partition_number is None:
1825         logging.warning("Could not detect partition number, not activating partition")
1826
1827     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1828     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1829     if mbr_device == "/dev/loop":
1830         mbr_device = device
1831         logging.info(
1832             "Detected loop device - using %s as MBR device therefore", mbr_device
1833         )
1834
1835     mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1836     if options.syslinuxmbr:
1837         mbrcode = ""
1838         mbr_locations = (
1839             "/usr/lib/syslinux/mbr.bin",
1840             "/usr/lib/syslinux/bios/mbr.bin",
1841             "/usr/share/syslinux/mbr.bin",
1842         )
1843         for mbrpath in mbr_locations:
1844             if os.path.isfile(mbrpath):
1845                 mbrcode = mbrpath
1846                 break
1847
1848         if not mbrcode:
1849             str_locations = " or ".join(['"%s"' % x for x in mbr_locations])
1850             logging.error("Cannot find syslinux MBR, install it at %s)", str_locations)
1851             raise CriticalException(
1852                 "syslinux MBR  can not be found at %s." % str_locations
1853             )
1854     elif options.mbrmenu:
1855         mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1856
1857     try:
1858         install_mbr(mbrcode, mbr_device, partition_number, True)
1859     except (IOError, Exception) as error:
1860         logging.critical("Execution failed: %s", error)
1861         sys.exit(1)
1862
1863
1864 def handle_vfat(device):
1865     """Check for FAT specific settings and options
1866
1867     @device: device that should checked / formated"""
1868
1869     # make sure we have mkfs.vfat available
1870     if options.fat16:
1871         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1872             logging.critical("Sorry, mkfs.vfat not available. Exiting.")
1873             logging.critical("Please make sure to install dosfstools.")
1874             sys.exit(1)
1875
1876         if options.force:
1877             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1878         else:
1879             # make sure the user is aware of what he is doing
1880             f = input(
1881                 "Are you sure you want to format the specified partition with fat16? y/N "
1882             )
1883             if f == "y" or f == "Y":
1884                 logging.info(
1885                     "Note: you can skip this question using the option --force"
1886                 )
1887             else:
1888                 sys.exit(1)
1889         try:
1890             mkfs_fat16(device)
1891         except CriticalException as error:
1892             logging.critical("Execution failed: %s", error)
1893             sys.exit(1)
1894
1895     # check for vfat filesystem
1896     if device is not None and not os.path.isdir(device) and options.syslinux:
1897         try:
1898             check_for_fat(device)
1899         except CriticalException as error:
1900             logging.critical("Execution failed: %s", error)
1901             sys.exit(1)
1902
1903     if options.skipusbcheck:
1904         logging.info(
1905             "Not checking for removable USB device as requested via option --skip-usb-check."
1906         )
1907         return
1908
1909     if (
1910         not os.path.isdir(device)
1911         and not check_for_usbdevice(device)
1912         and not options.force
1913     ):
1914         print(
1915             "Warning: the specified device %s does not look like a removable usb device."
1916             % device
1917         )
1918         f = input("Do you really want to continue? y/N ")
1919         if f.lower() != "y":
1920             sys.exit(1)
1921
1922
1923 def handle_compat_warning(device):
1924     """Backwards compatible checks
1925
1926     @device: device that should be checked"""
1927
1928     # make sure we can replace old grml2usb script and warn user when using old way of life:
1929     if (
1930         device.startswith("/mnt/external")
1931         or device.startswith("/mnt/usb")
1932         and not options.force
1933     ):
1934         print("Warning: the semantics of grml2usb has changed.")
1935         print("Instead of using grml2usb /path/to/iso %s you might" % device)
1936         print("want to use grml2usb /path/to/iso /dev/... instead.")
1937         print("Please check out the grml2usb manpage for details.")
1938         f = input("Do you really want to continue? y/N ")
1939         if f.lower() != "y":
1940             sys.exit(1)
1941
1942
1943 def handle_logging():
1944     """Log handling and configuration"""
1945
1946     if options.verbose and options.quiet:
1947         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1948
1949     FORMAT = "%(message)s"
1950     if options.verbose:
1951         FORMAT = "%(asctime)-15s %(message)s"
1952         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1953     elif options.quiet:
1954         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1955     else:
1956         logging.basicConfig(level=logging.INFO, format=FORMAT)
1957
1958
1959 def handle_bootloader(device):
1960     """wrapper for installing bootloader
1961
1962     @device: device where bootloader should be installed to"""
1963
1964     # Install bootloader only if not using the --copy-only option
1965     if options.copyonly:
1966         logging.info(
1967             "Not installing bootloader and its files as requested via option copyonly."
1968         )
1969     elif os.path.isdir(device):
1970         logging.info("Not installing bootloader as %s is a directory.", device)
1971     else:
1972         install_bootloader(device)
1973
1974
1975 def check_options(opts):
1976     """Check compatibility of provided user opts
1977
1978     @opts option dict from OptionParser
1979     """
1980     if opts.grubmbr and not opts.grub:
1981         raise CriticalException("--grub-mbr requires --grub option.")
1982
1983     if opts.copyonly and opts.grub:
1984         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
1985
1986
1987 def check_programs():
1988     """check if all needed programs are installed"""
1989     if options.grub:
1990         global GRUB_INSTALL
1991         GRUB_INSTALL = which("grub-install") or which("grub2-install")
1992         if not GRUB_INSTALL:
1993             logging.critical(
1994                 "Fatal: grub-install not available (please install the "
1995                 "grub package or drop the --grub option)"
1996             )
1997             sys.exit(1)
1998
1999     if options.syslinux:
2000         if not which("syslinux"):
2001             logging.critical(
2002                 "Fatal: syslinux not available (please install the "
2003                 "syslinux package or use the --grub option)"
2004             )
2005             sys.exit(1)
2006
2007     if not which("rsync"):
2008         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2009         sys.exit(1)
2010
2011
2012 def load_loop():
2013     """Runs modprobe loop and throws away its output"""
2014     if not which("modprobe"):
2015         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
2016         logging.critical("Hint: is /sbin missing in PATH?")
2017         sys.exit(1)
2018
2019     proc = subprocess.Popen(
2020         ["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2021     )
2022     proc.communicate()
2023
2024
2025 def main():
2026     """Main invocation"""
2027
2028     try:
2029         if options.version:
2030             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
2031             sys.exit(0)
2032
2033         if len(args) < 2:
2034             parser.error("invalid usage")
2035
2036         # log handling
2037         handle_logging()
2038
2039         # make sure we have the appropriate permissions
2040         check_uid_root()
2041
2042         check_options(options)
2043
2044         load_loop()
2045
2046         logging.info("Executing grml2usb version %s", PROG_VERSION)
2047
2048         if options.dryrun:
2049             logging.info("Running in simulation mode as requested via option dry-run.")
2050
2051         check_programs()
2052
2053         # specified arguments
2054         device = os.path.realpath(args[len(args) - 1])
2055         isos = args[0 : len(args) - 1]
2056
2057         if not os.path.isdir(device):
2058             if device[-1:].isdigit():
2059                 if int(device[-1:]) > 4 or device[-2:].isdigit():
2060                     logging.warning(
2061                         "Warning: installing on partition number >4, booting *might* fail depending on your system."
2062                     )
2063
2064         # provide upgrade path
2065         handle_compat_warning(device)
2066
2067         if not options.skipbootflag:
2068             check_boot_flag(device)
2069
2070         # check for vfat partition
2071         handle_vfat(device)
2072
2073         # main operation (like installing files)
2074         for iso in isos:
2075             install(iso, device)
2076
2077         # install mbr
2078         is_superfloppy = not device[-1:].isdigit()
2079         if is_superfloppy:
2080             logging.info("Detected superfloppy format - not installing MBR")
2081
2082         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
2083             handle_mbr(device)
2084
2085         handle_bootloader(device)
2086
2087         logging.info(
2088             "Note: grml flavour %s was installed as the default booting system.",
2089             GRML_DEFAULT,
2090         )
2091
2092         for flavour in GRML_FLAVOURS:
2093             logging.info(
2094                 "Note: you can boot flavour %s using '%s' on the commandline.",
2095                 flavour,
2096                 flavour,
2097             )
2098
2099         # finally be polite :)
2100         logging.info(
2101             "Finished execution of grml2usb (%s). Have fun with your Grml system.",
2102             PROG_VERSION,
2103         )
2104
2105     except Exception as error:
2106         logging.critical("Fatal: %s", str(error))
2107         if options.verbose:
2108             logging.exception("Exception:")
2109         sys.exit(1)
2110
2111
2112 if __name__ == "__main__":
2113     try:
2114         main()
2115     except KeyboardInterrupt:
2116         logging.info("Received KeyboardInterrupt")
2117         cleanup()
2118
2119 # END OF FILE ##################################################################
2120 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8