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