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