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