cb0eb9ec88cae02742091f9f6ea3e9398d58181a
[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:
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 - "
738                                     "please unmount before invoking grml2usb").format(source))
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(r'bootid=[\w_-]+')
1270     live_media_path_re = re.compile(r'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, "a+")
1327     data.seek(0)
1328     for line in data:
1329         if line == entry:
1330             break
1331     else:
1332         data.write(entry)
1333
1334     data.close()
1335
1336
1337 def get_flavour_filename(flavour):
1338     """Generate a iso9960 save filename out of the specified flavour
1339
1340     @flavour: grml flavour
1341     """
1342     return flavour.replace('-', '_')
1343
1344
1345 def adjust_syslinux_bootoptions(src, flavour):
1346     """Adjust existing bootoptions of specified syslinux config to
1347     grml2usb specific ones, e.g. change the location of the kernel...
1348
1349     @src: config file to alter
1350     @flavour: grml flavour
1351     """
1352
1353     append_re = re.compile(r'^(\s*append.*/boot/.*)$', re.I)
1354     # flavour_re = re.compile("(label.*)(grml\w+)")
1355     default_re = re.compile(r'(default.cfg)')
1356     bootid_re = re.compile(r'bootid=[\w_-]+')
1357     live_media_path_re = re.compile(r'live-media-path=[\w_/-]+')
1358
1359     bootopt = get_bootoptions(flavour)
1360
1361     regexe = []
1362     option_re = None
1363     if options.removeoption:
1364         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1365
1366         for regex in options.removeoption:
1367             regexe.append(re.compile(r'%s' % regex))
1368
1369     for line in fileinput.input(src, inplace=1):
1370         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1371         line = default_re.sub(r'%s-\1' % flavour, line)
1372         line = bootid_re.sub('', line)
1373         line = live_media_path_re.sub('', line)
1374         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1375         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1376         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1377         if option_re and option_re.search(line):
1378             for regex in regexe:
1379                 line = regex.sub(' ', line)
1380         sys.stdout.write(line)
1381     fileinput.close()
1382
1383
1384 def adjust_labels(src, replacement):
1385     """Adjust the specified labels in the syslinux config file src with
1386     specified replacement
1387     """
1388     label_re = re.compile(r'^(\s*label\s*) ([a-zA-Z0-9_-]+)', re.I)
1389     for line in fileinput.input(src, inplace=1):
1390         line = label_re.sub(replacement, line)
1391         sys.stdout.write(line)
1392     fileinput.close()
1393
1394
1395 def add_syslinux_entry(filename, grml_flavour):
1396     """Add includes for a specific grml_flavour to the specified filename
1397
1398     @filename: syslinux config file
1399     @grml_flavour: grml flavour to add
1400     """
1401
1402     entry_filename = "option_%s.cfg" % grml_flavour
1403     entry = "include %s\n" % entry_filename
1404
1405     add_entry_if_not_present(filename, entry)
1406     path = os.path.dirname(filename)
1407
1408     data = open(path + "/" + entry_filename, "w")
1409     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1410     data.close()
1411
1412
1413 def modify_filenames(grml_flavour, target, filenames):
1414     """Replace the standard filenames with the new ones
1415
1416     @grml_flavour: grml-flavour strin
1417     @target: directory where the files are located
1418     @filenames: list of filenames to alter
1419     """
1420     grml_filename = get_flavour_filename(grml_flavour)
1421     for filename in filenames:
1422         old_filename = "%s/%s" % (target, filename)
1423         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1424         os.rename(old_filename, new_filename)
1425
1426
1427 def remove_default_entry(filename):
1428     """Remove the default entry from specified syslinux file
1429
1430     @filename: syslinux config file
1431     """
1432     default_re = re.compile(r'^(\s*menu\s*default\s*)$', re.I)
1433     for line in fileinput.input(filename, inplace=1):
1434         if default_re.match(line):
1435             continue
1436         sys.stdout.write(line)
1437     fileinput.close()
1438
1439
1440 def handle_syslinux_config(grml_flavour, target):
1441     """Main handler for generating syslinux configuration
1442
1443     @grml_flavour: name of grml flavour the configuration should be generated for
1444     @target: path of syslinux's configuration files"""
1445
1446     logging.debug("Generating syslinux configuration")
1447     syslinux_target = target + '/boot/syslinux/'
1448     # should be present via  copy_bootloader_files(), but make sure it exits:
1449     execute(mkdir, syslinux_target)
1450     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1451
1452     # install main configuration only *once*, no matter how many ISOs we have:
1453     syslinux_config_file = open(syslinux_cfg, 'w')
1454     syslinux_config_file.write("timeout 300\n")
1455     syslinux_config_file.write("include vesamenu.cfg\n")
1456     syslinux_config_file.close()
1457
1458     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1459     prompt_name.write('menu label S^yslinux prompt\n')
1460     prompt_name.close()
1461
1462     initial_syslinux_config(syslinux_target)
1463     flavour_filename = get_flavour_filename(grml_flavour)
1464
1465     if search_file('default.cfg', syslinux_target):
1466         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1467
1468     filename = search_file("new_hidden.cfg", syslinux_target)
1469
1470     # process hidden file
1471     if not search_file("hidden.cfg", syslinux_target):
1472         new_hidden = syslinux_target + "hidden.cfg"
1473         os.rename(filename, new_hidden)
1474         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1475     else:
1476         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1477         os.rename(filename, new_hidden_file)
1478         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1479         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1480         entry = 'include %s_hidden.cfg\n' % flavour_filename
1481         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1482
1483     new_default = "%s_default.cfg" % (flavour_filename)
1484     entry = 'include %s\n' % new_default
1485     defaults_file = '%s/defaults.cfg' % syslinux_target
1486     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1487     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1488
1489     if os.path.isfile(defaults_file):
1490
1491         # remove default menu entry in menu
1492         remove_default_entry(new_default_with_path)
1493
1494         # adjust all labels for additional isos
1495         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1496         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1497
1498     # always adjust bootoptions
1499     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1500     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1501
1502     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1503
1504     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1505
1506
1507 def handle_secure_boot(target, efi_img):
1508     """Provide secure boot support by extracting files from /boot/efi.img
1509
1510     @target: path where grml's main files should be copied to
1511     @efi_img: path to the efi.img file that includes the files for secure boot
1512     """
1513
1514     mkdir(target + '/efi/boot/')
1515     efi_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1516     logging.debug("efi_mountpoint = %s" % efi_mountpoint)
1517     register_tmpfile(efi_mountpoint)
1518
1519     try:
1520         logging.debug("mount(%s, %s, ['-o', 'ro', '-t', 'vfat']" % (efi_img, efi_mountpoint))
1521         mount(efi_img, efi_mountpoint, ['-o', 'ro', '-t', 'vfat'])
1522     except CriticalException as error:
1523         logging.critical("Fatal: %s", error)
1524         sys.exit(1)
1525
1526     ubuntu_cfg = search_file('grub.cfg', efi_mountpoint + '/EFI/ubuntu')
1527     logging.debug("ubuntu_cfg = %s" % ubuntu_cfg)
1528     if not ubuntu_cfg:
1529         logging.info("No /EFI/ubuntu/grub.cfg found inside EFI image, looks like Secure Boot support is missing.")
1530     else:
1531         mkdir(target + '/efi/ubuntu')
1532         logging.debug("exec_rsync(%s, %s + '/efi/ubuntu/grub.cfg')" % (ubuntu_cfg, target))
1533         exec_rsync(ubuntu_cfg, target + '/efi/ubuntu/grub.cfg')
1534
1535         logging.debug("exec_rsync(%s + '/EFI/BOOT/grubx64.efi', %s + '/efi/boot/grubx64.efi')'" % (efi_mountpoint, target))
1536         exec_rsync(efi_mountpoint + '/EFI/BOOT/grubx64.efi', target + '/efi/boot/grubx64.efi')
1537
1538         # NOTE - we're overwriting /efi/boot/bootx64.efi from copy_bootloader_files here
1539         logging.debug("exec_rsync(%s + '/EFI/BOOT/bootx64.efi', %s + '/efi/boot/bootx64.efi')'" % (efi_mountpoint, target))
1540         exec_rsync(efi_mountpoint + '/EFI/BOOT/bootx64.efi', target + '/efi/boot/bootx64.efi')
1541
1542     try:
1543         unmount(efi_mountpoint, "")
1544         logging.debug('Unmounted %s' % efi_mountpoint)
1545         os.rmdir(efi_mountpoint)
1546         logging.debug('Removed directory %s' % efi_mountpoint)
1547     except Exception:
1548         logging.critical('RuntimeError while umount %s' % efi_mountpoint)
1549         sys.exit(1)
1550
1551
1552 def handle_bootloader_config(grml_flavour, device, target):
1553     """Main handler for generating bootloader's configuration
1554
1555     @grml_flavour: name of grml flavour the configuration should be generated for
1556     @device: device/partition where bootloader should be installed to
1557     @target: path of bootloader's configuration files"""
1558
1559     global UUID
1560     UUID = get_uuid(target)
1561     if options.skipsyslinuxconfig:
1562         logging.info("Skipping generation of syslinux configuration as requested.")
1563     else:
1564         try:
1565             handle_syslinux_config(grml_flavour, target)
1566         except CriticalException as error:
1567             logging.critical("Fatal: %s", error)
1568             sys.exit(1)
1569
1570     if options.skipgrubconfig:
1571         logging.info("Skipping generation of grub configuration as requested.")
1572     else:
1573         try:
1574             handle_grub_config(grml_flavour, device, target)
1575         except CriticalException as error:
1576             logging.critical("Fatal: %s", error)
1577             sys.exit(1)
1578
1579
1580 def install(image, device):
1581     """Install a grml image to the specified device
1582
1583     @image: directory or is file
1584     @device: partition or directory to install the device
1585     """
1586     iso_mountpoint = image
1587     remove_image_mountpoint = False
1588     if os.path.isdir(image):
1589         if options.force or os.path.exists(os.path.join(image, 'live')):
1590             logging.info("Using %s as install base", image)
1591         else:
1592             q = input("%s does not look like a Grml system. "
1593                 "Do you really want to use this image? y/N " % image)
1594             if q.lower() == 'y':
1595                 logging.info("Using %s as install base", image)
1596             else:
1597                 logging.info("Skipping install base %s", image)
1598     else:
1599         logging.info("Using ISO %s", image)
1600         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1601         register_tmpfile(iso_mountpoint)
1602         remove_image_mountpoint = True
1603         try:
1604             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1605         except CriticalException as error:
1606             logging.critical("Fatal: %s", error)
1607             sys.exit(1)
1608
1609     try:
1610         install_grml(iso_mountpoint, device)
1611     finally:
1612         if remove_image_mountpoint:
1613             try:
1614                 remove_mountpoint(iso_mountpoint)
1615             except CriticalException:
1616                 cleanup()
1617                 raise
1618
1619
1620 def install_grml(mountpoint, device):
1621     """Main logic for copying files of the currently running Grml system.
1622
1623     @mountpoint: directory where currently running live system resides (usually /run/live/medium)
1624     @device: partition where the specified ISO should be installed to"""
1625
1626     device_mountpoint = device
1627     if os.path.isdir(device):
1628         logging.info("Specified device is a directory, therefore not mounting.")
1629         remove_device_mountpoint = False
1630     else:
1631         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1632         register_tmpfile(device_mountpoint)
1633         remove_device_mountpoint = True
1634         try:
1635             check_for_fat(device)
1636             if not options.skipbootflag:
1637                 check_boot_flag(device)
1638
1639             set_rw(device)
1640             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1641         except CriticalException:
1642             mount(device, device_mountpoint, "")
1643     try:
1644         grml_flavours = identify_grml_flavour(mountpoint)
1645         for flavour in set(grml_flavours):
1646             if not flavour:
1647                 logging.warning("No valid flavour found, please check your iso")
1648             logging.info("Identified grml flavour \"%s\".", flavour)
1649             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1650             GRML_FLAVOURS.add(flavour)
1651     finally:
1652         if remove_device_mountpoint:
1653             remove_mountpoint(device_mountpoint)
1654
1655
1656 def remove_mountpoint(mountpoint):
1657     """remove a registered mountpoint
1658     """
1659
1660     try:
1661         unmount(mountpoint, "")
1662         if os.path.isdir(mountpoint):
1663             os.rmdir(mountpoint)
1664             unregister_tmpfile(mountpoint)
1665     except CriticalException:
1666         cleanup()
1667         raise
1668
1669
1670 def handle_mbr(device):
1671     """Main handler for installing master boot record (MBR)
1672
1673     @device: device where the MBR should be installed to"""
1674
1675     if options.dryrun:
1676         logging.info("Would install MBR")
1677         return 0
1678
1679     mbr_device, partition_number = get_device_from_partition(device)
1680     if partition_number is None:
1681         logging.warn("Could not detect partition number, not activating partition")
1682
1683     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1684     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1685     if mbr_device == "/dev/loop":
1686         mbr_device = device
1687         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1688
1689     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1690     if options.syslinuxmbr:
1691         mbrcode = ""
1692         mbr_locations = ('/usr/lib/syslinux/mbr.bin',
1693                          '/usr/lib/syslinux/bios/mbr.bin',
1694                          '/usr/share/syslinux/mbr.bin')
1695         for mbrpath in mbr_locations:
1696             if os.path.isfile(mbrpath):
1697                 mbrcode = mbrpath
1698                 break
1699
1700         if not mbrcode:
1701             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1702             logging.error('Cannot find syslinux MBR, install it at %s)',
1703                           str_locations)
1704             raise CriticalException("syslinux MBR  can not be found at %s."
1705                                     % str_locations)
1706     elif options.mbrmenu:
1707         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1708
1709     try:
1710         install_mbr(mbrcode, mbr_device, partition_number, True)
1711     except (IOError, Exception) as error:
1712         logging.critical("Execution failed: %s", error)
1713         sys.exit(1)
1714
1715
1716 def handle_vfat(device):
1717     """Check for FAT specific settings and options
1718
1719     @device: device that should checked / formated"""
1720
1721     # make sure we have mkfs.vfat available
1722     if options.fat16:
1723         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1724             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1725             logging.critical('Please make sure to install dosfstools.')
1726             sys.exit(1)
1727
1728         if options.force:
1729             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1730         else:
1731             # make sure the user is aware of what he is doing
1732             f = input("Are you sure you want to format the specified partition with fat16? y/N ")
1733             if f == "y" or f == "Y":
1734                 logging.info("Note: you can skip this question using the option --force")
1735             else:
1736                 sys.exit(1)
1737         try:
1738             mkfs_fat16(device)
1739         except CriticalException as error:
1740             logging.critical("Execution failed: %s", error)
1741             sys.exit(1)
1742
1743     # check for vfat filesystem
1744     if device is not None and not os.path.isdir(device) and options.syslinux:
1745         try:
1746             check_for_fat(device)
1747         except CriticalException as error:
1748             logging.critical("Execution failed: %s", error)
1749             sys.exit(1)
1750
1751     if options.skipusbcheck:
1752         logging.info("Not checking for removable USB device as requested via option --skip-usb-check.")
1753         return
1754
1755     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1756         print("Warning: the specified device %s does not look like a removable usb device." % device)
1757         f = input("Do you really want to continue? y/N ")
1758         if f.lower() != "y":
1759             sys.exit(1)
1760
1761
1762 def handle_compat_warning(device):
1763     """Backwards compatible checks
1764
1765     @device: device that should be checked"""
1766
1767     # make sure we can replace old grml2usb script and warn user when using old way of life:
1768     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1769         print("Warning: the semantics of grml2usb has changed.")
1770         print("Instead of using grml2usb /path/to/iso %s you might" % device)
1771         print("want to use grml2usb /path/to/iso /dev/... instead.")
1772         print("Please check out the grml2usb manpage for details.")
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_logging():
1779     """Log handling and configuration"""
1780
1781     if options.verbose and options.quiet:
1782         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1783
1784     FORMAT = "%(message)s"
1785     if options.verbose:
1786         FORMAT = "%(asctime)-15s %(message)s"
1787         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1788     elif options.quiet:
1789         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1790     else:
1791         logging.basicConfig(level=logging.INFO, format=FORMAT)
1792
1793
1794 def handle_bootloader(device):
1795     """wrapper for installing bootloader
1796
1797     @device: device where bootloader should be installed to"""
1798
1799     # Install bootloader only if not using the --copy-only option
1800     if options.copyonly:
1801         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1802     elif os.path.isdir(device):
1803         logging.info("Not installing bootloader as %s is a directory.", device)
1804     else:
1805         install_bootloader(device)
1806
1807
1808 def check_options(opts):
1809     """Check compatibility of provided user opts
1810
1811     @opts option dict from OptionParser
1812     """
1813     if opts.grubmbr and not opts.grub:
1814         raise CriticalException("--grub-mbr requires --grub option.")
1815
1816     if opts.copyonly and opts.grub:
1817         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
1818
1819
1820 def check_programs():
1821     """check if all needed programs are installed"""
1822     if options.grub:
1823         global GRUB_INSTALL
1824         GRUB_INSTALL = which("grub-install") or which("grub2-install")
1825         if not GRUB_INSTALL:
1826             logging.critical("Fatal: grub-install not available (please install the "
1827                              "grub package or drop the --grub option)")
1828             sys.exit(1)
1829
1830     if options.syslinux:
1831         if not which("syslinux"):
1832             logging.critical("Fatal: syslinux not available (please install the "
1833                              "syslinux package or use the --grub option)")
1834             sys.exit(1)
1835
1836     if not which("rsync"):
1837         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1838         sys.exit(1)
1839
1840
1841 def load_loop():
1842     """Runs modprobe loop and throws away its output"""
1843     if not which("modprobe"):
1844         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
1845         logging.critical("Hint: is /sbin missing in PATH?")
1846         sys.exit(1)
1847
1848     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1849     proc.communicate()
1850
1851
1852 def main():
1853     """Main function [make pylint happy :)]"""
1854
1855     try:
1856         if options.version:
1857             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
1858             sys.exit(0)
1859
1860         if len(args) < 2:
1861             parser.error("invalid usage")
1862
1863         # log handling
1864         handle_logging()
1865
1866         # make sure we have the appropriate permissions
1867         check_uid_root()
1868
1869         check_options(options)
1870
1871         load_loop()
1872
1873         logging.info("Executing grml2usb version %s", PROG_VERSION)
1874
1875         if options.dryrun:
1876             logging.info("Running in simulation mode as requested via option dry-run.")
1877
1878         check_programs()
1879
1880         # specified arguments
1881         device = os.path.realpath(args[len(args) - 1])
1882         isos = args[0:len(args) - 1]
1883
1884         if not os.path.isdir(device):
1885             if device[-1:].isdigit():
1886                 if int(device[-1:]) > 4 or device[-2:].isdigit():
1887                     logging.warn("Warning: installing on partition number >4, booting *might* fail depending on your system.")
1888
1889         # provide upgrade path
1890         handle_compat_warning(device)
1891
1892         # check for vfat partition
1893         handle_vfat(device)
1894
1895         # main operation (like installing files)
1896         for iso in isos:
1897             install(iso, device)
1898
1899         # install mbr
1900         is_superfloppy = not device[-1:].isdigit()
1901         if is_superfloppy:
1902             logging.info("Detected superfloppy format - not installing MBR")
1903
1904         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1905             handle_mbr(device)
1906
1907         handle_bootloader(device)
1908
1909         logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1910
1911         for flavour in GRML_FLAVOURS:
1912             logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1913
1914         # finally be polite :)
1915         logging.info("Finished execution of grml2usb (%s). Have fun with your Grml system.", PROG_VERSION)
1916
1917     except Exception as error:
1918         logging.critical("Fatal: %s", str(error))
1919         if options.verbose:
1920             logging.exception("Exception:")
1921         sys.exit(1)
1922
1923
1924 if __name__ == "__main__":
1925     try:
1926         main()
1927     except KeyboardInterrupt:
1928         logging.info("Received KeyboardInterrupt")
1929         cleanup()
1930
1931 # END OF FILE ##################################################################
1932 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8