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