b12cf88db62b2e380214eb715cd7692267bf0102
[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         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
808                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
809         filesystem = udev_info.communicate()[0].rstrip()
810
811         if filesystem != "vfat":
812             raise CriticalException(
813                     "Partition %s does not contain a FAT16 filesystem. "
814                     "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
815
816     except OSError:
817         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
818
819
820 def mkdir(directory):
821     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
822
823     # just silently pass as it's just fine it the directory exists
824     if not os.path.isdir(directory):
825         try:
826             os.makedirs(directory)
827         # pylint: disable-msg=W0704
828         except OSError:
829             pass
830
831
832 def exec_rsync(source, target):
833     """Simple wrapper around rsync to install files
834
835     @source: source file/directory
836     @target: target file/directory"""
837     logging.debug("Source: %s / Target: %s", source, target)
838     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
839     proc.wait()
840     if proc.returncode == 12:
841         logging.critical("Fatal: No space left on device")
842         cleanup()
843         sys.exit(1)
844
845     if proc.returncode != 0:
846         logging.critical("Fatal: could not install %s", source)
847         cleanup()
848         sys.exit(1)
849
850
851 def write_uuid(target_file):
852     """Generates an returns uuid and write it to the specified file
853
854     @target_file: filename to write the uuid to
855     """
856
857     fileh = open(target_file, 'w')
858     uid = str(uuid.uuid4())
859     fileh.write(uid)
860     fileh.close()
861     return uid
862
863
864 def get_uuid(target):
865     """Get the uuid of the specified target. Will generate an uuid if none exist.
866
867     @target: directory/mountpoint containing the grml layout
868     """
869
870     conf_target = target + "/conf/"
871     uuid_file_name = conf_target + "/bootid.txt"
872     if os.path.isdir(conf_target):
873         if os.path.isfile(uuid_file_name):
874             uuid_file = open(uuid_file_name, 'r')
875             uid = uuid_file.readline().strip()
876             uuid_file.close()
877             return uid
878         else:
879             return write_uuid(uuid_file_name)
880     else:
881         execute(mkdir, conf_target)
882         return write_uuid(uuid_file_name)
883
884
885 def get_shortname(grml_flavour):
886     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
887     @grml_flavour: flavour name which shold be translated to shortname"""
888
889     return re.sub(r'[,._-]', '', grml_flavour)
890
891
892 def copy_system_files(grml_flavour, iso_mount, target):
893     """copy grml's main files (like squashfs, kernel and initrd) to a given target
894
895     @grml_flavour: name of grml flavour the configuration should be generated for
896     @iso_mount: path where a grml ISO is mounted on
897     @target: path where grml's main files should be copied to"""
898
899     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
900     if squashfs is None:
901         logging.error("error locating squashfs file")
902         raise CriticalException("squashfs file not found, please check that your iso is not corrupt")
903     else:
904         squashfs_target = target + '/live/' + grml_flavour + '/'
905         execute(mkdir, squashfs_target)
906     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
907
908     for prefix in grml_flavour + "/", "":
909         filesystem_module = search_file(prefix + 'filesystem.module', iso_mount)
910         if filesystem_module:
911             break
912     if filesystem_module is None:
913         logging.error("error locating filesystem.module file")
914         raise CriticalException("filesystem.module not found")
915     else:
916         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
917
918     shortname = get_shortname(grml_flavour)
919     if os.path.isdir(iso_mount + '/boot/' + shortname):
920         exec_rsync(iso_mount + '/boot/' + shortname, target + '/boot')
921     else:
922         kernel = search_file('vmlinuz', iso_mount)
923         if kernel is None:
924             # compat for releases < 2011.12
925             kernel = search_file('linux26', iso_mount)
926
927         if kernel is None:
928             logging.error("error locating kernel file")
929             raise CriticalException("Kernel not found")
930
931         source = os.path.dirname(kernel) + '/'
932         dest = target + '/' + os.path.dirname(kernel).replace(iso_mount, '') + '/'
933         execute(mkdir, dest)
934         exec_rsync(source, dest)
935
936
937 def update_grml_versions(iso_mount, target):
938     """Update the grml version file on a cd
939     Returns true if version was updated successfully,
940     False if grml-version does not exist yet on the mountpoint
941
942     @iso_mount: string of the iso mount point
943     @target: path of the target mount point
944     """
945     grml_target = target + '/grml/'
946     target_grml_version_file = search_file('grml-version', grml_target)
947     if target_grml_version_file:
948         iso_grml_version_file = search_file('grml-version', iso_mount)
949         if not iso_grml_version_file:
950             logging.warn("Warning: %s could not be found - can not install it", iso_grml_version_file)
951             return False
952         try:
953             # read the flavours from the iso image
954             iso_versions = {}
955             iso_file = open(iso_grml_version_file, 'r')
956             for line in iso_file:
957                 iso_versions[get_flavour(line)] = line.strip()
958
959             # update the existing flavours on the target
960             for line in fileinput.input([target_grml_version_file], inplace=1):
961                 flavour = get_flavour(line)
962                 if flavour in list(iso_versions.keys()):
963                     print(iso_versions.pop(flavour))
964                 else:
965                     print(line.strip())
966             fileinput.close()
967
968             target_file = open(target_grml_version_file, 'a')
969             # add the new flavours from the current iso
970             for flavour in iso_versions:
971                 target_file.write("%s\n" % iso_versions[flavour])
972         except IOError:
973             logging.warn("Warning: Could not write file")
974         finally:
975             iso_file.close()
976             target_file.close()
977         return True
978     else:
979         return False
980
981
982 def copy_grml_files(grml_flavour, iso_mount, target):
983     """copy some minor grml files to a given target
984
985     @grml_flavour: the current grml_flavour
986     @iso_mount: path where a grml ISO is mounted on
987     @target: path where grml's main files should be copied to"""
988
989     grml_target = target + '/grml/'
990     execute(mkdir, grml_target)
991
992     grml_prefixe = ["GRML", "grml"]
993     for prefix in grml_prefixe:
994         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
995         if os.path.exists(filename):
996             exec_rsync(filename, grml_target)
997             break
998     else:
999         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
1000
1001
1002 def copy_addons(iso_mount, target):
1003     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1004
1005     @iso_mount: path where a grml ISO is mounted on
1006     @target: path where grml's main files should be copied to"""
1007
1008     addons = target + '/boot/addons/'
1009     execute(mkdir, addons)
1010
1011     for addon_file in glob.glob(iso_mount + '/boot/addons/*'):
1012         filename = os.path.basename(addon_file)
1013         src_file = iso_mount + "/boot/addons/" + os.path.basename(addon_file)
1014         logging.debug("Copying addon file %s" % filename)
1015         exec_rsync(src_file, addons)
1016
1017
1018 def build_loopbackcfg(target):
1019     """Generate GRUB's loopback.cfg based on existing config files.
1020
1021     @target: target directory
1022     """
1023
1024     grub_dir = '/boot/grub/'
1025     mkdir(os.path.join(target, grub_dir))
1026
1027     f = open(target + grub_dir + 'loopback.cfg', 'w')
1028
1029     f.write("# grml2usb generated grub2 configuration file\n")
1030     f.write("source /boot/grub/header.cfg\n")
1031
1032     for defaults in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"):
1033         sourcefile = defaults.split(target + os.path.sep)[1]
1034         logging.debug("Found source file" + sourcefile)
1035         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1036
1037     for ops in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"):
1038         sourcefile = ops.split(target + os.path.sep)[1]
1039         logging.debug("Found source file" + sourcefile)
1040         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1041
1042     f.write("source /boot/grub/addons.cfg\n")
1043     f.write("source /boot/grub/footer.cfg\n")
1044     f.close()
1045
1046
1047 def glob_and_copy(filepattern, dst):
1048     """Glob on specified filepattern and copy the result to dst
1049
1050     @filepattern: globbing pattern
1051     @dst: target directory
1052     """
1053     for name in glob.glob(filepattern):
1054         copy_if_exist(name, dst)
1055
1056
1057 def search_and_copy(filename, search_path, dst):
1058     """Search for the specified filename at searchpath and copy it to dst
1059
1060     @filename: filename to look for
1061     @search_path: base search file
1062     @dst: destionation to copy the file to
1063     """
1064     file_location = search_file(filename, search_path)
1065     copy_if_exist(file_location, dst)
1066
1067
1068 def copy_if_exist(filename, dst):
1069     """Copy filename to dst if filename is set.
1070
1071     @filename: a filename
1072     @dst: dst file
1073     """
1074     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1075         exec_rsync(filename, dst)
1076
1077
1078 def copy_bootloader_files(iso_mount, target, grml_flavour):
1079     """Copy grml's bootloader files to a given target
1080
1081     @iso_mount: path where a grml ISO is mounted on
1082     @target: path where grml's main files should be copied to
1083     @grml_flavour: name of the current processed grml_flavour
1084     """
1085
1086     syslinux_target = target + '/boot/syslinux/'
1087     execute(mkdir, syslinux_target)
1088
1089     grub_target = target + '/boot/grub/'
1090     execute(mkdir, grub_target)
1091
1092     logo = search_file('logo.16', iso_mount, required=True)
1093     exec_rsync(logo, syslinux_target + 'logo.16')
1094
1095     bootx64_efi = search_file('bootx64.efi', iso_mount)
1096     if bootx64_efi:
1097         mkdir(target + '/efi/boot/')
1098         exec_rsync(bootx64_efi, target + '/efi/boot/bootx64.efi')
1099
1100     efi_img = search_file('efi.img', iso_mount)
1101     if efi_img:
1102         mkdir(target + '/boot/')
1103         exec_rsync(efi_img, target + '/boot/efi.img')
1104         handle_secure_boot(target, efi_img)
1105
1106     for ffile in ['f%d' % number for number in range(1, 11)]:
1107         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1108
1109     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1110     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1111     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1112         os.unlink(syslinux_target + 'ldlinux.sys')
1113
1114     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1115     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1116
1117     if not source_dir:
1118         raise CriticalException(
1119             "file default.cfg could not be found.\n"
1120             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1121             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1122
1123     if not os.path.exists(iso_mount + '/boot/grub/footer.cfg'):
1124         logging.warning("Warning: Grml releases older than 2011.12 support only one flavour in grub.")
1125
1126     for expr in name, 'distri.cfg', \
1127       defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1128       'isoprompt.cfg', 'options.cfg', \
1129       'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1130         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1131
1132     for filename in glob.glob1(syslinux_target, "*.c32"):
1133         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1134
1135     # copy the addons_*.cfg file to the new syslinux directory
1136     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1137
1138     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1139
1140     # copy all grub files from ISO
1141     glob_and_copy(iso_mount + '/boot/grub/*', grub_target)
1142
1143     # finally (after all GRUB files have been installed) build static loopback.cfg
1144     build_loopbackcfg(target)
1145
1146
1147 def install_iso_files(grml_flavour, iso_mount, device, target):
1148     """Copy files from ISO to given target
1149
1150     @grml_flavour: name of grml flavour the configuration should be generated for
1151     @iso_mount: path where a grml ISO is mounted on
1152     @device: device/partition where bootloader should be installed to
1153     @target: path where grml's main files should be copied to"""
1154
1155     global GRML_DEFAULT
1156     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1157     if options.dryrun:
1158         return 0
1159     elif not options.bootloaderonly:
1160         logging.info("Copying files. This might take a while....")
1161         try:
1162             copy_system_files(grml_flavour, iso_mount, target)
1163             copy_grml_files(grml_flavour, iso_mount, target)
1164         except CriticalException as error:
1165             logging.critical("Execution failed: %s", error)
1166             sys.exit(1)
1167
1168     if not options.skipaddons:
1169         if not search_file('addons', iso_mount):
1170             logging.info("Could not find addons, therefore not installing.")
1171         else:
1172             copy_addons(iso_mount, target)
1173
1174     if not options.copyonly:
1175         copy_bootloader_files(iso_mount, target, grml_flavour)
1176
1177         if not options.dryrun:
1178             handle_bootloader_config(grml_flavour, device, target)
1179
1180     # make sure we sync filesystems before returning
1181     proc = subprocess.Popen(["sync"])
1182     proc.wait()
1183
1184
1185 def get_device_from_partition(partition):
1186     device = partition
1187     partition_number = None
1188     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1189         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1190         if m:
1191             device = m.group(1)
1192             partition_number = int(m.group(2)) - 1
1193     return (device, partition_number)
1194
1195
1196 def get_flavour(flavour_str):
1197     """Returns the flavour of a grml version string
1198     """
1199     return re.match(r'[\w-]*', flavour_str).group()
1200
1201
1202 def identify_grml_flavour(mountpath):
1203     """Get name of grml flavour
1204
1205     @mountpath: path where the grml ISO is mounted to
1206     @return: name of grml-flavour"""
1207
1208     version_files = search_file('grml-version', mountpath, lst_return=True)
1209
1210     if not version_files:
1211         if mountpath.startswith("/run/live/medium"):
1212             logging.critical("Error: could not find grml-version file.")
1213             logging.critical("Looks like your system is running from RAM but required files are not available.")
1214             logging.critical("Please either boot without toram=... or use boot option toram instead of toram=...")
1215         else:
1216             logging.critical("Error: could not find grml-version file.")
1217         cleanup()
1218         sys.exit(1)
1219
1220     flavours = []
1221     logging.debug("version_files = %s", version_files)
1222     for version_file in version_files:
1223         tmpfile = None
1224         try:
1225             tmpfile = open(version_file, 'r')
1226             for line in tmpfile.readlines():
1227                 flavours.append(get_flavour(line))
1228         finally:
1229             if tmpfile:
1230                 tmpfile.close()
1231
1232     return flavours
1233
1234
1235 def get_bootoptions(grml_flavour):
1236     """Returns bootoptions for specific flavour
1237
1238     @grml_flavour: name of the grml_flavour
1239     """
1240     # do NOT write "None" in kernel cmdline
1241     if not options.bootoptions:
1242         bootopt = ""
1243     else:
1244         bootopt = " ".join(options.bootoptions)
1245     bootopt = bootopt.replace("%flavour", grml_flavour)
1246     return bootopt
1247
1248
1249 def handle_grub_config(grml_flavour, device, target):
1250     """Main handler for generating grub (v1 and v2) configuration
1251
1252     @grml_flavour: name of grml flavour the configuration should be generated for
1253     @device: device/partition where grub should be installed to
1254     @target: path of grub's configuration files"""
1255
1256     global UUID
1257
1258     logging.debug("Updating grub configuration")
1259
1260     grub_target = target + '/boot/grub/'
1261     secureboot_target = target + '/EFI/ubuntu/'
1262
1263     bootid_re = re.compile("bootid=[\w_-]+")
1264     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1265
1266     bootopt = get_bootoptions(grml_flavour)
1267
1268     remove_regexes = []
1269     option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1270
1271     if options.removeoption:
1272         for regex in options.removeoption:
1273             remove_regexes.append(re.compile(regex))
1274
1275     shortname = get_shortname(grml_flavour)
1276     for filename in glob.glob(grub_target + '*.cfg') + glob.glob(secureboot_target + '*.cfg'):
1277         for line in fileinput.input(filename, inplace=1):
1278             line = line.rstrip("\r\n")
1279             if option_re.search(line):
1280                 line = bootid_re.sub('', line)
1281                 if shortname in filename:
1282                     line = live_media_path_re.sub('', line)
1283                     line = line.rstrip() + ' live-media-path=/live/%s/ ' % (grml_flavour)
1284                 if bootopt.strip():
1285                     line = line.replace(' {} '.format(bootopt.strip()), ' ')
1286                     if line.endswith(bootopt):
1287                         line = line[:-len(bootopt)]
1288                 line = line.rstrip() + r' bootid=%s %s ' % (UUID, bootopt)
1289                 for regex in remove_regexes:
1290                     line = regex.sub(' ', line)
1291             print(line)
1292         fileinput.close()
1293
1294
1295 def initial_syslinux_config(target):
1296     """Generates initial syslinux configuration
1297
1298     @target path of syslinux's configuration files"""
1299
1300     target = target + "/"
1301     filename = target + "grmlmain.cfg"
1302     if os.path.isfile(target + "grmlmain.cfg"):
1303         return
1304     data = open(filename, "w")
1305     data.write(generate_main_syslinux_config())
1306     data.close()
1307
1308     filename = target + "hiddens.cfg"
1309     data = open(filename, "w")
1310     data.write("include hidden.cfg\n")
1311     data.close()
1312
1313
1314 def add_entry_if_not_present(filename, entry):
1315     """Write entry into filename if entry is not already in the file
1316
1317     @filename: name of the file
1318     @entry: data to write to the file
1319     """
1320     data = open(filename, "a+")
1321     for line in data:
1322         if line == entry:
1323             break
1324     else:
1325         data.write(entry)
1326
1327     data.close()
1328
1329
1330 def get_flavour_filename(flavour):
1331     """Generate a iso9960 save filename out of the specified flavour
1332
1333     @flavour: grml flavour
1334     """
1335     return flavour.replace('-', '_')
1336
1337
1338 def adjust_syslinux_bootoptions(src, flavour):
1339     """Adjust existing bootoptions of specified syslinux config to
1340     grml2usb specific ones, e.g. change the location of the kernel...
1341
1342     @src: config file to alter
1343     @flavour: grml flavour
1344     """
1345
1346     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1347     # flavour_re = re.compile("(label.*)(grml\w+)")
1348     default_re = re.compile("(default.cfg)")
1349     bootid_re = re.compile("bootid=[\w_-]+")
1350     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1351
1352     bootopt = get_bootoptions(flavour)
1353
1354     regexe = []
1355     option_re = None
1356     if options.removeoption:
1357         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1358
1359         for regex in options.removeoption:
1360             regexe.append(re.compile(r'%s' % regex))
1361
1362     for line in fileinput.input(src, inplace=1):
1363         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1364         line = default_re.sub(r'%s-\1' % flavour, line)
1365         line = bootid_re.sub('', line)
1366         line = live_media_path_re.sub('', line)
1367         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1368         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1369         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1370         if option_re and option_re.search(line):
1371             for regex in regexe:
1372                 line = regex.sub(' ', line)
1373         sys.stdout.write(line)
1374     fileinput.close()
1375
1376
1377 def adjust_labels(src, replacement):
1378     """Adjust the specified labels in the syslinux config file src with
1379     specified replacement
1380     """
1381     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1382     for line in fileinput.input(src, inplace=1):
1383         line = label_re.sub(replacement, line)
1384         sys.stdout.write(line)
1385     fileinput.close()
1386
1387
1388 def add_syslinux_entry(filename, grml_flavour):
1389     """Add includes for a specific grml_flavour to the specified filename
1390
1391     @filename: syslinux config file
1392     @grml_flavour: grml flavour to add
1393     """
1394
1395     entry_filename = "option_%s.cfg" % grml_flavour
1396     entry = "include %s\n" % entry_filename
1397
1398     add_entry_if_not_present(filename, entry)
1399     path = os.path.dirname(filename)
1400
1401     data = open(path + "/" + entry_filename, "w")
1402     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1403     data.close()
1404
1405
1406 def modify_filenames(grml_flavour, target, filenames):
1407     """Replace the standard filenames with the new ones
1408
1409     @grml_flavour: grml-flavour strin
1410     @target: directory where the files are located
1411     @filenames: list of filenames to alter
1412     """
1413     grml_filename = get_flavour_filename(grml_flavour)
1414     for filename in filenames:
1415         old_filename = "%s/%s" % (target, filename)
1416         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1417         os.rename(old_filename, new_filename)
1418
1419
1420 def remove_default_entry(filename):
1421     """Remove the default entry from specified syslinux file
1422
1423     @filename: syslinux config file
1424     """
1425     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1426     for line in fileinput.input(filename, inplace=1):
1427         if default_re.match(line):
1428             continue
1429         sys.stdout.write(line)
1430     fileinput.close()
1431
1432
1433 def handle_syslinux_config(grml_flavour, target):
1434     """Main handler for generating syslinux configuration
1435
1436     @grml_flavour: name of grml flavour the configuration should be generated for
1437     @target: path of syslinux's configuration files"""
1438
1439     logging.debug("Generating syslinux configuration")
1440     syslinux_target = target + '/boot/syslinux/'
1441     # should be present via  copy_bootloader_files(), but make sure it exits:
1442     execute(mkdir, syslinux_target)
1443     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1444
1445     # install main configuration only *once*, no matter how many ISOs we have:
1446     syslinux_config_file = open(syslinux_cfg, 'w')
1447     syslinux_config_file.write("timeout 300\n")
1448     syslinux_config_file.write("include vesamenu.cfg\n")
1449     syslinux_config_file.close()
1450
1451     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1452     prompt_name.write('menu label S^yslinux prompt\n')
1453     prompt_name.close()
1454
1455     initial_syslinux_config(syslinux_target)
1456     flavour_filename = get_flavour_filename(grml_flavour)
1457
1458     if search_file('default.cfg', syslinux_target):
1459         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1460
1461     filename = search_file("new_hidden.cfg", syslinux_target)
1462
1463     # process hidden file
1464     if not search_file("hidden.cfg", syslinux_target):
1465         new_hidden = syslinux_target + "hidden.cfg"
1466         os.rename(filename, new_hidden)
1467         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1468     else:
1469         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1470         os.rename(filename, new_hidden_file)
1471         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1472         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1473         entry = 'include %s_hidden.cfg\n' % flavour_filename
1474         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1475
1476     new_default = "%s_default.cfg" % (flavour_filename)
1477     entry = 'include %s\n' % new_default
1478     defaults_file = '%s/defaults.cfg' % syslinux_target
1479     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1480     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1481
1482     if os.path.isfile(defaults_file):
1483
1484         # remove default menu entry in menu
1485         remove_default_entry(new_default_with_path)
1486
1487         # adjust all labels for additional isos
1488         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1489         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1490
1491     # always adjust bootoptions
1492     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1493     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1494
1495     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1496
1497     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1498
1499
1500 def handle_secure_boot(target, efi_img):
1501     """Provide secure boot support by extracting files from /boot/efi.img
1502
1503     @target: path where grml's main files should be copied to
1504     @efi_img: path to the efi.img file that includes the files for secure boot
1505     """
1506
1507     mkdir(target + '/efi/boot/')
1508     efi_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1509     logging.debug("efi_mountpoint = %s" % efi_mountpoint)
1510     register_tmpfile(efi_mountpoint)
1511
1512     try:
1513         logging.debug("mount(%s, %s, ['-o', 'ro', '-t', 'vfat']" % (efi_img, efi_mountpoint))
1514         mount(efi_img, efi_mountpoint, ['-o', 'ro', '-t', 'vfat'])
1515     except CriticalException as error:
1516         logging.critical("Fatal: %s", error)
1517         sys.exit(1)
1518
1519     ubuntu_cfg = search_file('grub.cfg', efi_mountpoint + '/EFI/ubuntu')
1520     logging.debug("ubuntu_cfg = %s" % ubuntu_cfg)
1521     if not ubuntu_cfg:
1522         logging.info("No /EFI/ubuntu/grub.cfg found inside EFI image, looks like Secure Boot support is missing.")
1523     else:
1524         mkdir(target + '/efi/ubuntu')
1525         logging.debug("exec_rsync(%s, %s + '/efi/ubuntu/grub.cfg')" % (ubuntu_cfg, target))
1526         exec_rsync(ubuntu_cfg, target + '/efi/ubuntu/grub.cfg')
1527
1528         logging.debug("exec_rsync(%s + '/EFI/BOOT/grubx64.efi', %s + '/efi/boot/grubx64.efi')'" % (efi_mountpoint, target))
1529         exec_rsync(efi_mountpoint + '/EFI/BOOT/grubx64.efi', target + '/efi/boot/grubx64.efi')
1530
1531         # NOTE - we're overwriting /efi/boot/bootx64.efi from copy_bootloader_files here
1532         logging.debug("exec_rsync(%s + '/EFI/BOOT/bootx64.efi', %s + '/efi/boot/bootx64.efi')'" % (efi_mountpoint, target))
1533         exec_rsync(efi_mountpoint + '/EFI/BOOT/bootx64.efi', target + '/efi/boot/bootx64.efi')
1534
1535     try:
1536         unmount(efi_mountpoint, "")
1537         logging.debug('Unmounted %s' % efi_mountpoint)
1538         os.rmdir(efi_mountpoint)
1539         logging.debug('Removed directory %s' % efi_mountpoint)
1540     except Exception:
1541         logging.critical('RuntimeError while umount %s' % efi_mountpoint)
1542         sys.exit(1)
1543
1544
1545 def handle_bootloader_config(grml_flavour, device, target):
1546     """Main handler for generating bootloader's configuration
1547
1548     @grml_flavour: name of grml flavour the configuration should be generated for
1549     @device: device/partition where bootloader should be installed to
1550     @target: path of bootloader's configuration files"""
1551
1552     global UUID
1553     UUID = get_uuid(target)
1554     if options.skipsyslinuxconfig:
1555         logging.info("Skipping generation of syslinux configuration as requested.")
1556     else:
1557         try:
1558             handle_syslinux_config(grml_flavour, target)
1559         except CriticalException as error:
1560             logging.critical("Fatal: %s", error)
1561             sys.exit(1)
1562
1563     if options.skipgrubconfig:
1564         logging.info("Skipping generation of grub configuration as requested.")
1565     else:
1566         try:
1567             handle_grub_config(grml_flavour, device, target)
1568         except CriticalException as error:
1569             logging.critical("Fatal: %s", error)
1570             sys.exit(1)
1571
1572
1573 def install(image, device):
1574     """Install a grml image to the specified device
1575
1576     @image: directory or is file
1577     @device: partition or directory to install the device
1578     """
1579     iso_mountpoint = image
1580     remove_image_mountpoint = False
1581     if os.path.isdir(image):
1582         if options.force or os.path.exists(os.path.join(image, 'live')):
1583             logging.info("Using %s as install base", image)
1584         else:
1585             q = input("%s does not look like a Grml system. "
1586                 "Do you really want to use this image? y/N " % image)
1587             if q.lower() == 'y':
1588                 logging.info("Using %s as install base", image)
1589             else:
1590                 logging.info("Skipping install base %s", image)
1591     else:
1592         logging.info("Using ISO %s", image)
1593         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1594         register_tmpfile(iso_mountpoint)
1595         remove_image_mountpoint = True
1596         try:
1597             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1598         except CriticalException as error:
1599             logging.critical("Fatal: %s", error)
1600             sys.exit(1)
1601
1602     try:
1603         install_grml(iso_mountpoint, device)
1604     finally:
1605         if remove_image_mountpoint:
1606             try:
1607                 remove_mountpoint(iso_mountpoint)
1608             except CriticalException as error:
1609                 cleanup()
1610                 raise
1611
1612
1613 def install_grml(mountpoint, device):
1614     """Main logic for copying files of the currently running Grml system.
1615
1616     @mountpoint: directory where currently running live system resides (usually /run/live/medium)
1617     @device: partition where the specified ISO should be installed to"""
1618
1619     device_mountpoint = device
1620     if os.path.isdir(device):
1621         logging.info("Specified device is a directory, therefore not mounting.")
1622         remove_device_mountpoint = False
1623     else:
1624         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1625         register_tmpfile(device_mountpoint)
1626         remove_device_mountpoint = True
1627         try:
1628             check_for_fat(device)
1629             if not options.skipbootflag:
1630                 check_boot_flag(device)
1631
1632             set_rw(device)
1633             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1634         except CriticalException as error:
1635             mount(device, device_mountpoint, "")
1636     try:
1637         grml_flavours = identify_grml_flavour(mountpoint)
1638         for flavour in set(grml_flavours):
1639             if not flavour:
1640                 logging.warning("No valid flavour found, please check your iso")
1641             logging.info("Identified grml flavour \"%s\".", flavour)
1642             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1643             GRML_FLAVOURS.add(flavour)
1644     finally:
1645         if remove_device_mountpoint:
1646             remove_mountpoint(device_mountpoint)
1647
1648
1649 def remove_mountpoint(mountpoint):
1650     """remove a registered mountpoint
1651     """
1652
1653     try:
1654         unmount(mountpoint, "")
1655         if os.path.isdir(mountpoint):
1656             os.rmdir(mountpoint)
1657             unregister_tmpfile(mountpoint)
1658     except CriticalException as error:
1659         cleanup()
1660         raise
1661
1662
1663 def handle_mbr(device):
1664     """Main handler for installing master boot record (MBR)
1665
1666     @device: device where the MBR should be installed to"""
1667
1668     if options.dryrun:
1669         logging.info("Would install MBR")
1670         return 0
1671
1672     mbr_device, partition_number = get_device_from_partition(device)
1673     if partition_number is None:
1674         logging.warn("Could not detect partition number, not activating partition")
1675
1676     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1677     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1678     if mbr_device == "/dev/loop":
1679         mbr_device = device
1680         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1681
1682     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1683     if options.syslinuxmbr:
1684         mbrcode = ""
1685         mbr_locations = ('/usr/lib/syslinux/mbr.bin',
1686                          '/usr/lib/syslinux/bios/mbr.bin',
1687                          '/usr/share/syslinux/mbr.bin')
1688         for mbrpath in mbr_locations:
1689             if os.path.isfile(mbrpath):
1690                 mbrcode = mbrpath
1691                 break
1692
1693         if mbrcode is "":
1694             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1695             logging.error('Cannot find syslinux MBR, install it at %s)',
1696                           str_locations)
1697             raise CriticalException("syslinux MBR  can not be found at %s."
1698                                     % str_locations)
1699     elif options.mbrmenu:
1700         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1701
1702     try:
1703         install_mbr(mbrcode, mbr_device, partition_number, True)
1704     except (IOError, Exception) as error:
1705         logging.critical("Execution failed: %s", error)
1706         sys.exit(1)
1707
1708
1709 def handle_vfat(device):
1710     """Check for FAT specific settings and options
1711
1712     @device: device that should checked / formated"""
1713
1714     # make sure we have mkfs.vfat available
1715     if options.fat16:
1716         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1717             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1718             logging.critical('Please make sure to install dosfstools.')
1719             sys.exit(1)
1720
1721         if options.force:
1722             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1723         else:
1724             # make sure the user is aware of what he is doing
1725             f = input("Are you sure you want to format the specified partition with fat16? y/N ")
1726             if f == "y" or f == "Y":
1727                 logging.info("Note: you can skip this question using the option --force")
1728             else:
1729                 sys.exit(1)
1730         try:
1731             mkfs_fat16(device)
1732         except CriticalException as error:
1733             logging.critical("Execution failed: %s", error)
1734             sys.exit(1)
1735
1736     # check for vfat filesystem
1737     if device is not None and not os.path.isdir(device) and options.syslinux:
1738         try:
1739             check_for_fat(device)
1740         except CriticalException as error:
1741             logging.critical("Execution failed: %s", error)
1742             sys.exit(1)
1743
1744     if options.skipusbcheck:
1745         logging.info("Not checking for removable USB device as requested via option --skip-usb-check.")
1746         return
1747
1748     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1749         print("Warning: the specified device %s does not look like a removable usb device." % device)
1750         f = input("Do you really want to continue? y/N ")
1751         if f.lower() != "y":
1752             sys.exit(1)
1753
1754
1755 def handle_compat_warning(device):
1756     """Backwards compatible checks
1757
1758     @device: device that should be checked"""
1759
1760     # make sure we can replace old grml2usb script and warn user when using old way of life:
1761     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1762         print("Warning: the semantics of grml2usb has changed.")
1763         print("Instead of using grml2usb /path/to/iso %s you might" % device)
1764         print("want to use grml2usb /path/to/iso /dev/... instead.")
1765         print("Please check out the grml2usb manpage for details.")
1766         f = input("Do you really want to continue? y/N ")
1767         if f.lower() != "y":
1768             sys.exit(1)
1769
1770
1771 def handle_logging():
1772     """Log handling and configuration"""
1773
1774     if options.verbose and options.quiet:
1775         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1776
1777     FORMAT = "%(message)s"
1778     if options.verbose:
1779         FORMAT = "%(asctime)-15s %(message)s"
1780         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1781     elif options.quiet:
1782         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1783     else:
1784         logging.basicConfig(level=logging.INFO, format=FORMAT)
1785
1786
1787 def handle_bootloader(device):
1788     """wrapper for installing bootloader
1789
1790     @device: device where bootloader should be installed to"""
1791
1792     # Install bootloader only if not using the --copy-only option
1793     if options.copyonly:
1794         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1795     elif os.path.isdir(device):
1796         logging.info("Not installing bootloader as %s is a directory.", device)
1797     else:
1798         install_bootloader(device)
1799
1800
1801 def check_options(opts):
1802     """Check compatibility of provided user opts
1803
1804     @opts option dict from OptionParser
1805     """
1806     if opts.grubmbr and not opts.grub:
1807         raise CriticalException("--grub-mbr requires --grub option.")
1808
1809     if opts.copyonly and opts.grub:
1810         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
1811
1812
1813 def check_programs():
1814     """check if all needed programs are installed"""
1815     if options.grub:
1816         global GRUB_INSTALL
1817         GRUB_INSTALL = which("grub-install") or which("grub2-install")
1818         if not GRUB_INSTALL:
1819             logging.critical("Fatal: grub-install not available (please install the " +
1820                              "grub package or drop the --grub option)")
1821             sys.exit(1)
1822
1823     if options.syslinux:
1824         if not which("syslinux"):
1825             logging.critical("Fatal: syslinux not available (please install the " +
1826                              "syslinux package or use the --grub option)")
1827             sys.exit(1)
1828
1829     if not which("rsync"):
1830         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1831         sys.exit(1)
1832
1833
1834 def load_loop():
1835     """Runs modprobe loop and throws away its output"""
1836     if not which("modprobe"):
1837         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
1838         logging.critical("Hint: is /sbin missing in PATH?")
1839         sys.exit(1)
1840
1841     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1842     proc.communicate()
1843
1844
1845 def main():
1846     """Main function [make pylint happy :)]"""
1847
1848     try:
1849         if options.version:
1850             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
1851             sys.exit(0)
1852
1853         if len(args) < 2:
1854             parser.error("invalid usage")
1855
1856         # log handling
1857         handle_logging()
1858
1859         # make sure we have the appropriate permissions
1860         check_uid_root()
1861
1862         check_options(options)
1863
1864         load_loop()
1865
1866         logging.info("Executing grml2usb version %s", PROG_VERSION)
1867
1868         if options.dryrun:
1869             logging.info("Running in simulation mode as requested via option dry-run.")
1870
1871         check_programs()
1872
1873         # specified arguments
1874         device = os.path.realpath(args[len(args) - 1])
1875         isos = args[0:len(args) - 1]
1876
1877         if not os.path.isdir(device):
1878             if device[-1:].isdigit():
1879                 if int(device[-1:]) > 4 or device[-2:].isdigit():
1880                     logging.warn("Warning: installing on partition number >4, booting *might* fail depending on your system.")
1881
1882         # provide upgrade path
1883         handle_compat_warning(device)
1884
1885         # check for vfat partition
1886         handle_vfat(device)
1887
1888         # main operation (like installing files)
1889         for iso in isos:
1890             install(iso, device)
1891
1892         # install mbr
1893         is_superfloppy = not device[-1:].isdigit()
1894         if is_superfloppy:
1895             logging.info("Detected superfloppy format - not installing MBR")
1896
1897         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1898             handle_mbr(device)
1899
1900         handle_bootloader(device)
1901
1902         logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1903
1904         for flavour in GRML_FLAVOURS:
1905             logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1906
1907         # finally be polite :)
1908         logging.info("Finished execution of grml2usb (%s). Have fun with your Grml system.", PROG_VERSION)
1909
1910     except Exception as error:
1911         logging.critical("Fatal: %s", str(error))
1912         if options.verbose:
1913             logging.exception("Exception:")
1914         sys.exit(1)
1915
1916
1917 if __name__ == "__main__":
1918     try:
1919         main()
1920     except KeyboardInterrupt:
1921         logging.info("Received KeyboardInterrupt")
1922         cleanup()
1923
1924 # END OF FILE ##################################################################
1925 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8