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