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