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