Option --rw-blockdev: set r/w also in GRUB specific steps
[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             # If using --grub-mbr then make sure we install grub in MBR instead of PBR
506             if options.grubmbr:
507                 logging.debug("Using option --grub-mbr ...")
508                 grub_device, x = get_device_from_partition(device)
509             else:
510                 grub_device = device
511
512             set_rw(device)
513             mount(device, device_mountpoint, "")
514
515             logging.info("Installing grub as bootloader")
516             for opt in ["", "--force"]:
517                 set_rw(device)
518                 set_rw(grub_device)
519                 logging.debug("grub-install --recheck %s --no-floppy --root-directory=%s %s",
520                               opt, device_mountpoint, grub_device)
521                 proc = subprocess.Popen([GRUB_INSTALL, "--recheck", opt,
522                                          "--no-floppy", "--target=i386-pc",
523                                          "--root-directory=%s" % device_mountpoint, grub_device],
524                                         stdout=file(os.devnull, "r+"))
525                 proc.wait()
526                 if proc.returncode == 0:
527                     break
528
529             if proc.returncode != 0:
530                 # raise Exception("error executing grub-install")
531                 logging.critical("Fatal: error executing grub-install " +
532                                  "(please check the grml2usb FAQ or drop the --grub option)")
533                 logging.critical("Note:  if using grub2 consider using " +
534                                  "the --grub-mbr option as grub considers PBR problematic.")
535                 cleanup()
536                 sys.exit(1)
537         except CriticalException as error:
538             logging.critical("Fatal: %s", error)
539             cleanup()
540             sys.exit(1)
541         finally:
542             unmount(device_mountpoint, "")
543             os.rmdir(device_mountpoint)
544             unregister_tmpfile(device_mountpoint)
545
546
547 def install_syslinux(device):
548     """Install syslinux on specified device.
549
550     @device: partition where syslinux should be installed to"""
551
552     if options.dryrun:
553         logging.info("Would install syslinux as bootloader on %s", device)
554         return 0
555
556     set_rw(device)
557
558     # syslinux -d boot/isolinux /dev/sdb1
559     logging.info("Installing syslinux as bootloader")
560     logging.debug("syslinux -d boot/syslinux %s", device)
561     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
562     proc.wait()
563     if proc.returncode != 0:
564         raise CriticalException("Error executing syslinux (either try --fat16 or use grub?)")
565
566
567 def install_bootloader(device):
568     """Install bootloader on specified device.
569
570     @device: partition where bootloader should be installed to"""
571
572     # by default we use grub, so install syslinux only on request
573     if options.grub:
574         try:
575             install_grub(device)
576         except CriticalException as error:
577             logging.critical("Fatal: %s", error)
578             cleanup()
579             sys.exit(1)
580     else:
581         try:
582             install_syslinux(device)
583         except CriticalException as error:
584             logging.critical("Fatal: %s", error)
585             cleanup()
586             sys.exit(1)
587
588
589 def install_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
590     """install 'mbr' master boot record (MBR) on a device
591
592     Retrieve the partition table from "device", install an MBR from the
593     "mbrtemplate" file, set the "partition" (0..3) active, and install the
594     result back to "device".
595
596     @mbrtemplate: default MBR file
597
598     @device: name of a file assumed to be a hard disc (or USB stick) image, or
599     something like "/dev/sdb"
600
601     @partition: must be a number between 0 and 3, inclusive
602
603     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
604     ismirbsdmbr) bytes.
605
606     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
607     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
608     false then leave the mirbsdmbr specific flag set to FFh, set all
609     active flags to 0 and set the active flag of the partition to 80h.  Note:
610     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
611     the active flag."""
612
613     logging.info("Installing default MBR")
614
615     if not os.path.isfile(mbrtemplate):
616         logging.error('Error installing MBR (either try --syslinux-mbr or '
617             'install missing file "%s"?)', mbrtemplate)
618         raise CriticalException("%s can not be read." % mbrtemplate)
619
620     if partition is not None and ((partition < 0) or (partition > 3)):
621         logging.warn("Cannot activate partition %d", partition)
622         partition = None
623
624     if ismirbsdmbr:
625         nmbrbytes = 439
626     else:
627         nmbrbytes = 440
628
629     tmpf = tempfile.NamedTemporaryFile()
630
631     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
632     proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
633                             stderr=file(os.devnull, "r+"))
634     proc.wait()
635     if proc.returncode != 0:
636         raise Exception("error executing dd (first run)")
637
638     logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc", mbrtemplate,
639                   tmpf.name, nmbrbytes)
640     proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes,
641                              "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
642     proc.wait()
643     if proc.returncode != 0:
644         raise Exception("error executing dd (second run)")
645
646     mbrcode = tmpf.file.read(512)
647     if len(mbrcode) < 512:
648         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
649
650     if partition is not None:
651         if ismirbsdmbr:
652             mbrcode = mbrcode[0:439] + chr(partition) + \
653                     mbrcode[440:510] + "\x55\xAA"
654         else:
655             actives = ["\x00", "\x00", "\x00", "\x00"]
656             actives[partition] = "\x80"
657             mbrcode = mbrcode[0:446] + actives[0] + \
658                     mbrcode[447:462] + actives[1] + \
659                     mbrcode[463:478] + actives[2] + \
660                     mbrcode[479:494] + actives[3] + \
661                     mbrcode[495:510] + "\x55\xAA"
662
663     tmpf.file.seek(0)
664     tmpf.file.truncate()
665     tmpf.file.write(mbrcode)
666     tmpf.file.close()
667
668     set_rw(device)
669
670     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device)
671     proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % device, "bs=512", "count=1",
672                              "conv=notrunc"], stderr=file(os.devnull, "r+"))
673     proc.wait()
674     if proc.returncode != 0:
675         raise Exception("error executing dd (third run)")
676     del tmpf
677
678     # make sure we sync filesystems before returning
679     proc = subprocess.Popen(["sync"])
680     proc.wait()
681
682     set_rw(device)
683
684
685 def is_writeable(device):
686     """Check if the device is writeable for the current user
687
688     @device: partition where bootloader should be installed to"""
689
690     if not device:
691         return False
692
693     if not os.path.exists(device):
694         return False
695
696     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
697
698
699 def mount(source, target, mount_options):
700     """Mount specified source on given target
701
702     @source: name of device/ISO that should be mounted
703     @target: directory where the ISO should be mounted to
704     @options: mount specific options"""
705
706     # note: options.dryrun does not work here, as we have to
707     # locate files and identify the grml flavour
708
709     for x in file('/proc/mounts').readlines():
710         if x.startswith(source):
711             raise CriticalException("Error executing mount: %s already mounted - " % source +
712                                     "please unmount before invoking grml2usb")
713
714     if os.path.isdir(source):
715         logging.debug("Source %s is not a device, therefore not mounting.", source)
716         return 0
717
718     logging.debug("mount %s %s %s", mount_options, source, target)
719     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
720     proc.wait()
721     if proc.returncode != 0:
722         raise CriticalException("Error executing mount (no filesystem on the partition?)")
723     else:
724         logging.debug("register_mountpoint(%s)", target)
725         register_mountpoint(target)
726
727
728 def unmount(target, unmount_options):
729     """Unmount specified target
730
731     @target: target where something is mounted on and which should be unmounted
732     @options: options for umount command"""
733
734     # make sure we unmount only already mounted targets
735     target_unmount = False
736     mounts = open('/proc/mounts').readlines()
737     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
738     for line in mounts:
739         if re.match(mountstring, line):
740             target_unmount = True
741
742     if not target_unmount:
743         logging.debug("%s not mounted anymore", target)
744     else:
745         logging.debug("umount %s %s", list(unmount_options), target)
746         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
747         proc.wait()
748         if proc.returncode != 0:
749             raise Exception("Error executing umount")
750         else:
751             logging.debug("unregister_mountpoint(%s)", target)
752             unregister_mountpoint(target)
753
754
755 def check_for_usbdevice(device):
756     """Check whether the specified device is a removable USB device
757
758     @device: device name, like /dev/sda1 or /dev/sda
759     """
760
761     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
762     # newer systems:
763     usbdev = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
764     if not os.path.isfile(usbdev):
765         # Ubuntu with kernel 2.6.24 for example:
766         usbdev = os.path.realpath('/sys/block/' + usbdevice + '/removable')
767
768     if os.path.isfile(usbdev):
769         is_usb = open(usbdev).readline()
770         if is_usb.find("1"):
771             return 0
772
773     return 1
774
775
776 def check_for_fat(partition):
777     """Check whether specified partition is a valid VFAT/FAT16 filesystem
778
779     @partition: device name of partition"""
780
781     if not os.access(partition, os.R_OK):
782         raise CriticalException("Failed to read device %s"
783                 " (wrong UID/permissions or device/directory not present?)" % partition)
784
785     try:
786         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
787                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
788         filesystem = udev_info.communicate()[0].rstrip()
789
790         if filesystem != "vfat":
791             raise CriticalException(
792                     "Partition %s does not contain a FAT16 filesystem. "
793                     "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
794
795     except OSError:
796         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
797
798
799 def mkdir(directory):
800     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
801
802     # just silently pass as it's just fine it the directory exists
803     if not os.path.isdir(directory):
804         try:
805             os.makedirs(directory)
806         # pylint: disable-msg=W0704
807         except OSError:
808             pass
809
810
811 def exec_rsync(source, target):
812     """Simple wrapper around rsync to install files
813
814     @source: source file/directory
815     @target: target file/directory"""
816     logging.debug("Source: %s / Target: %s", source, target)
817     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
818     proc.wait()
819     if proc.returncode == 12:
820         logging.critical("Fatal: No space left on device")
821         cleanup()
822         sys.exit(1)
823
824     if proc.returncode != 0:
825         logging.critical("Fatal: could not install %s", source)
826         cleanup()
827         sys.exit(1)
828
829
830 def write_uuid(target_file):
831     """Generates an returns uuid and write it to the specified file
832
833     @target_file: filename to write the uuid to
834     """
835
836     fileh = open(target_file, 'w')
837     uid = str(uuid.uuid4())
838     fileh.write(uid)
839     fileh.close()
840     return uid
841
842
843 def get_uuid(target):
844     """Get the uuid of the specified target. Will generate an uuid if none exist.
845
846     @target: directory/mountpoint containing the grml layout
847     """
848
849     conf_target = target + "/conf/"
850     uuid_file_name = conf_target + "/bootid.txt"
851     if os.path.isdir(conf_target):
852         if os.path.isfile(uuid_file_name):
853             uuid_file = open(uuid_file_name, 'r')
854             uid = uuid_file.readline().strip()
855             uuid_file.close()
856             return uid
857         else:
858             return write_uuid(uuid_file_name)
859     else:
860         execute(mkdir, conf_target)
861         return write_uuid(uuid_file_name)
862
863
864 def get_shortname(grml_flavour):
865     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
866     @grml_flavour: flavour name which shold be translated to shortname"""
867
868     return re.sub(r'[,._-]', '', grml_flavour)
869
870
871 def copy_system_files(grml_flavour, iso_mount, target):
872     """copy grml's main files (like squashfs, kernel and initrd) to a given target
873
874     @grml_flavour: name of grml flavour the configuration should be generated for
875     @iso_mount: path where a grml ISO is mounted on
876     @target: path where grml's main files should be copied to"""
877
878     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
879     if squashfs is None:
880         logging.error("error locating squashfs file")
881         raise CriticalException("squashfs file not found, please check that your iso is not corrupt")
882     else:
883         squashfs_target = target + '/live/' + grml_flavour + '/'
884         execute(mkdir, squashfs_target)
885     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
886
887     for prefix in grml_flavour + "/", "":
888         filesystem_module = search_file(prefix + 'filesystem.module', iso_mount)
889         if filesystem_module:
890             break
891     if filesystem_module is None:
892         logging.error("error locating filesystem.module file")
893         raise CriticalException("filesystem.module not found")
894     else:
895         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
896
897     shortname = get_shortname(grml_flavour)
898     if os.path.isdir(iso_mount + '/boot/' + shortname):
899         exec_rsync(iso_mount + '/boot/' + shortname, target + '/boot')
900     else:
901         kernel = search_file('vmlinuz', iso_mount)
902         if kernel is None:
903             # compat for releases < 2011.12
904             kernel = search_file('linux26', iso_mount)
905
906         if kernel is None:
907             logging.error("error locating kernel file")
908             raise CriticalException("Kernel not found")
909
910         source = os.path.dirname(kernel) + '/'
911         dest = target + '/' + os.path.dirname(kernel).replace(iso_mount, '') + '/'
912         execute(mkdir, dest)
913         exec_rsync(source, dest)
914
915
916 def update_grml_versions(iso_mount, target):
917     """Update the grml version file on a cd
918     Returns true if version was updated successfully,
919     False if grml-version does not exist yet on the mountpoint
920
921     @iso_mount: string of the iso mount point
922     @target: path of the target mount point
923     """
924     grml_target = target + '/grml/'
925     target_grml_version_file = search_file('grml-version', grml_target)
926     if target_grml_version_file:
927         iso_grml_version_file = search_file('grml-version', iso_mount)
928         if not iso_grml_version_file:
929             logging.warn("Warning: %s could not be found - can not install it", iso_grml_version_file)
930             return False
931         try:
932             # read the flavours from the iso image
933             iso_versions = {}
934             iso_file = open(iso_grml_version_file, 'r')
935             for line in iso_file:
936                 iso_versions[get_flavour(line)] = line.strip()
937
938             # update the existing flavours on the target
939             for line in fileinput.input([target_grml_version_file], inplace=1):
940                 flavour = get_flavour(line)
941                 if flavour in iso_versions.keys():
942                     print(iso_versions.pop(flavour))
943                 else:
944                     print(line.strip())
945             fileinput.close()
946
947             target_file = open(target_grml_version_file, 'a')
948             # add the new flavours from the current iso
949             for flavour in iso_versions:
950                 target_file.write("%s\n" % iso_versions[flavour])
951         except IOError:
952             logging.warn("Warning: Could not write file")
953         finally:
954             iso_file.close()
955             target_file.close()
956         return True
957     else:
958         return False
959
960
961 def copy_grml_files(grml_flavour, iso_mount, target):
962     """copy some minor grml files to a given target
963
964     @grml_flavour: the current grml_flavour
965     @iso_mount: path where a grml ISO is mounted on
966     @target: path where grml's main files should be copied to"""
967
968     grml_target = target + '/grml/'
969     execute(mkdir, grml_target)
970
971     grml_prefixe = ["GRML", "grml"]
972     for prefix in grml_prefixe:
973         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
974         if os.path.exists(filename):
975             exec_rsync(filename, grml_target)
976             break
977     else:
978         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
979
980
981 def handle_addon_copy(filename, dst, iso_mount, ignore_errors=False):
982     """handle copy of optional addons
983
984     @filename: filename of the addon
985     @dst: destination directory
986     @iso_mount: location of the iso mount
987     @ignore_errors: don't report missing files
988     """
989     file_location = search_file(filename, iso_mount)
990     if file_location is None:
991         if not ignore_errors:
992             logging.warn("Warning: %s not found (that's fine if you don't need it)", filename)
993     else:
994         exec_rsync(file_location, dst)
995
996
997 def copy_addons(iso_mount, target):
998     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
999
1000     @iso_mount: path where a grml ISO is mounted on
1001     @target: path where grml's main files should be copied to"""
1002
1003     addons = target + '/boot/addons/'
1004     execute(mkdir, addons)
1005
1006     # grub all-in-one image
1007     handle_addon_copy('allinone.img', addons, iso_mount)
1008
1009     # bsd image
1010     handle_addon_copy('bsd4grml', addons, iso_mount)
1011
1012     # DOS image
1013     handle_addon_copy('balder10.imz', addons, iso_mount)
1014
1015     # syslinux + pci.ids for hdt
1016     for expr in '*.c32', 'pci.ids':
1017         glob_and_copy(iso_mount + '/boot/addons/' + expr, addons)
1018
1019     # memdisk image
1020     handle_addon_copy('memdisk', addons, iso_mount)
1021
1022     # memtest86+ image
1023     handle_addon_copy('memtest', addons, iso_mount)
1024
1025     # gpxe.lkrn: got replaced by ipxe
1026     handle_addon_copy('gpxe.lkrn', addons, iso_mount, ignore_errors=True)
1027
1028     # ipxe.lkrn
1029     handle_addon_copy('ipxe.lkrn', addons, iso_mount)
1030
1031     # netboot.xyz
1032     handle_addon_copy('netboot.xyz.lkrn', addons, iso_mount)
1033
1034
1035 def build_loopbackcfg(target):
1036     """Generate GRUB's loopback.cfg based on existing config files.
1037
1038     @target: target directory
1039     """
1040
1041     grub_dir = '/boot/grub/'
1042     mkdir(os.path.join(target, grub_dir))
1043
1044     f = open(target + grub_dir + 'loopback.cfg', 'w')
1045
1046     f.write("# grml2usb generated grub2 configuration file\n")
1047     f.write("source /boot/grub/header.cfg\n")
1048
1049     for defaults in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"):
1050         sourcefile = defaults.split(target + os.path.sep)[1]
1051         logging.debug("Found source file" + sourcefile)
1052         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1053
1054     for ops in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"):
1055         sourcefile = ops.split(target + os.path.sep)[1]
1056         logging.debug("Found source file" + sourcefile)
1057         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1058
1059     f.write("source /boot/grub/addons.cfg\n")
1060     f.write("source /boot/grub/footer.cfg\n")
1061     f.close()
1062
1063
1064 def glob_and_copy(filepattern, dst):
1065     """Glob on specified filepattern and copy the result to dst
1066
1067     @filepattern: globbing pattern
1068     @dst: target directory
1069     """
1070     for name in glob.glob(filepattern):
1071         copy_if_exist(name, dst)
1072
1073
1074 def search_and_copy(filename, search_path, dst):
1075     """Search for the specified filename at searchpath and copy it to dst
1076
1077     @filename: filename to look for
1078     @search_path: base search file
1079     @dst: destionation to copy the file to
1080     """
1081     file_location = search_file(filename, search_path)
1082     copy_if_exist(file_location, dst)
1083
1084
1085 def copy_if_exist(filename, dst):
1086     """Copy filename to dst if filename is set.
1087
1088     @filename: a filename
1089     @dst: dst file
1090     """
1091     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1092         exec_rsync(filename, dst)
1093
1094
1095 def copy_bootloader_files(iso_mount, target, grml_flavour):
1096     """Copy grml's bootloader files to a given target
1097
1098     @iso_mount: path where a grml ISO is mounted on
1099     @target: path where grml's main files should be copied to
1100     @grml_flavour: name of the current processed grml_flavour
1101     """
1102
1103     syslinux_target = target + '/boot/syslinux/'
1104     execute(mkdir, syslinux_target)
1105
1106     grub_target = target + '/boot/grub/'
1107     execute(mkdir, grub_target)
1108
1109     logo = search_file('logo.16', iso_mount)
1110     exec_rsync(logo, syslinux_target + 'logo.16')
1111
1112     bootx64_efi = search_file('bootx64.efi', iso_mount)
1113     if bootx64_efi:
1114         mkdir(target + '/efi/boot/')
1115         exec_rsync(bootx64_efi, target + '/efi/boot/bootx64.efi')
1116
1117     efi_img = search_file('efi.img', iso_mount)
1118     if efi_img:
1119         mkdir(target + '/boot/')
1120         exec_rsync(efi_img, target + '/boot/efi.img')
1121
1122     for ffile in ['f%d' % number for number in range(1, 11)]:
1123         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1124
1125     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1126     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1127     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1128         os.unlink(syslinux_target + 'ldlinux.sys')
1129
1130     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1131     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1132
1133     if not source_dir:
1134         raise CriticalException(
1135             "file default.cfg could not be found.\n"
1136             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1137             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1138
1139     if not os.path.exists(iso_mount + '/boot/grub/footer.cfg'):
1140         logging.warning("Warning: Grml releases older than 2011.12 support only one flavour in grub.")
1141
1142     for expr in name, 'distri.cfg', \
1143       defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1144       'isoprompt.cfg', 'options.cfg', \
1145       'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1146         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1147
1148     for filename in glob.glob1(syslinux_target, "*.c32"):
1149         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1150
1151     # copy the addons_*.cfg file to the new syslinux directory
1152     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1153
1154     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1155
1156     # copy all grub files from ISO
1157     glob_and_copy(iso_mount + '/boot/grub/*', grub_target)
1158
1159     # finally (after all GRUB files have been been installed) build static loopback.cfg
1160     build_loopbackcfg(target)
1161
1162
1163 def install_iso_files(grml_flavour, iso_mount, device, target):
1164     """Copy files from ISO to given target
1165
1166     @grml_flavour: name of grml flavour the configuration should be generated for
1167     @iso_mount: path where a grml ISO is mounted on
1168     @device: device/partition where bootloader should be installed to
1169     @target: path where grml's main files should be copied to"""
1170
1171     global GRML_DEFAULT
1172     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1173     if options.dryrun:
1174         return 0
1175     elif not options.bootloaderonly:
1176         logging.info("Copying files. This might take a while....")
1177         try:
1178             copy_system_files(grml_flavour, iso_mount, target)
1179             copy_grml_files(grml_flavour, iso_mount, target)
1180         except CriticalException as error:
1181             logging.critical("Execution failed: %s", error)
1182             sys.exit(1)
1183
1184     if not options.skipaddons:
1185         if not search_file('addons', iso_mount):
1186             logging.info("Could not find addons, therefore not installing.")
1187         else:
1188             copy_addons(iso_mount, target)
1189
1190     if not options.copyonly:
1191         copy_bootloader_files(iso_mount, target, grml_flavour)
1192
1193         if not options.dryrun:
1194             handle_bootloader_config(grml_flavour, device, target)
1195
1196     # make sure we sync filesystems before returning
1197     proc = subprocess.Popen(["sync"])
1198     proc.wait()
1199
1200
1201 def get_device_from_partition(partition):
1202     device = partition
1203     partition_number = None
1204     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1205         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1206         if m:
1207             device = m.group(1)
1208             partition_number = int(m.group(2)) - 1
1209     return (device, partition_number)
1210
1211
1212 def get_flavour(flavour_str):
1213     """Returns the flavour of a grml version string
1214     """
1215     return re.match(r'[\w-]*', flavour_str).group()
1216
1217
1218 def identify_grml_flavour(mountpath):
1219     """Get name of grml flavour
1220
1221     @mountpath: path where the grml ISO is mounted to
1222     @return: name of grml-flavour"""
1223
1224     version_files = search_file('grml-version', mountpath, lst_return=True)
1225
1226     if not version_files:
1227         if mountpath.startswith("/lib/live/mount/medium"):
1228             logging.critical("Error: could not find grml-version file.")
1229             logging.critical("Looks like your system is running from RAM but required files are not available.")
1230             logging.critical("Please either boot without toram=... or use boot option toram instead of toram=...")
1231         else:
1232             logging.critical("Error: could not find grml-version file.")
1233         cleanup()
1234         sys.exit(1)
1235
1236     flavours = []
1237     logging.debug("version_files = %s", version_files)
1238     for version_file in version_files:
1239         tmpfile = None
1240         try:
1241             tmpfile = open(version_file, 'r')
1242             for line in tmpfile.readlines():
1243                 flavours.append(get_flavour(line))
1244         finally:
1245             if tmpfile:
1246                 tmpfile.close()
1247
1248     return flavours
1249
1250
1251 def get_bootoptions(grml_flavour):
1252     """Returns bootoptions for specific flavour
1253
1254     @grml_flavour: name of the grml_flavour
1255     """
1256     # do NOT write "None" in kernel cmdline
1257     if not options.bootoptions:
1258         bootopt = ""
1259     else:
1260         bootopt = " ".join(options.bootoptions)
1261     bootopt = bootopt.replace("%flavour", grml_flavour)
1262     return bootopt
1263
1264
1265 def handle_grub_config(grml_flavour, device, target):
1266     """Main handler for generating grub (v1 and v2) configuration
1267
1268     @grml_flavour: name of grml flavour the configuration should be generated for
1269     @device: device/partition where grub should be installed to
1270     @target: path of grub's configuration files"""
1271
1272     global UUID
1273
1274     logging.debug("Updating grub configuration")
1275
1276     grub_target = target + '/boot/grub/'
1277
1278     bootid_re = re.compile("bootid=[\w_-]+")
1279     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1280
1281     bootopt = get_bootoptions(grml_flavour)
1282
1283     remove_regexes = []
1284     option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1285
1286     if options.removeoption:
1287         for regex in options.removeoption:
1288             remove_regexes.append(re.compile(regex))
1289
1290     shortname = get_shortname(grml_flavour)
1291     for filename in glob.glob(grub_target + '*.cfg'):
1292         for line in fileinput.input(filename, inplace=1):
1293             line = line.rstrip("\r\n")
1294             if option_re.search(line):
1295                 line = bootid_re.sub('', line)
1296                 if shortname in filename:
1297                     line = live_media_path_re.sub('', line)
1298                     line = line.rstrip() + ' live-media-path=/live/%s/ ' % (grml_flavour)
1299                 line = line.rstrip() + r' bootid=%s %s ' % (UUID, bootopt)
1300                 for regex in remove_regexes:
1301                     line = regex.sub(' ', line)
1302             print(line)
1303         fileinput.close()
1304
1305
1306 def initial_syslinux_config(target):
1307     """Generates intial syslinux configuration
1308
1309     @target path of syslinux's configuration files"""
1310
1311     target = target + "/"
1312     filename = target + "grmlmain.cfg"
1313     if os.path.isfile(target + "grmlmain.cfg"):
1314         return
1315     data = open(filename, "w")
1316     data.write(generate_main_syslinux_config())
1317     data.close()
1318
1319     filename = target + "hiddens.cfg"
1320     data = open(filename, "w")
1321     data.write("include hidden.cfg\n")
1322     data.close()
1323
1324
1325 def add_entry_if_not_present(filename, entry):
1326     """Write entry into filename if entry is not already in the file
1327
1328     @filename: name of the file
1329     @entry: data to write to the file
1330     """
1331     data = open(filename, "a+")
1332     for line in data:
1333         if line == entry:
1334             break
1335     else:
1336         data.write(entry)
1337
1338     data.close()
1339
1340
1341 def get_flavour_filename(flavour):
1342     """Generate a iso9960 save filename out of the specified flavour
1343
1344     @flavour: grml flavour
1345     """
1346     return flavour.replace('-', '_')
1347
1348
1349 def adjust_syslinux_bootoptions(src, flavour):
1350     """Adjust existing bootoptions of specified syslinux config to
1351     grml2usb specific ones, e.g. change the location of the kernel...
1352
1353     @src: config file to alter
1354     @flavour: grml flavour
1355     """
1356
1357     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1358     # flavour_re = re.compile("(label.*)(grml\w+)")
1359     default_re = re.compile("(default.cfg)")
1360     bootid_re = re.compile("bootid=[\w_-]+")
1361     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1362
1363     bootopt = get_bootoptions(flavour)
1364
1365     regexe = []
1366     option_re = None
1367     if options.removeoption:
1368         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1369
1370         for regex in options.removeoption:
1371             regexe.append(re.compile(r'%s' % regex))
1372
1373     for line in fileinput.input(src, inplace=1):
1374         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1375         line = default_re.sub(r'%s-\1' % flavour, line)
1376         line = bootid_re.sub('', line)
1377         line = live_media_path_re.sub('', line)
1378         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1379         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1380         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1381         if option_re and option_re.search(line):
1382             for regex in regexe:
1383                 line = regex.sub(' ', line)
1384         sys.stdout.write(line)
1385     fileinput.close()
1386
1387
1388 def adjust_labels(src, replacement):
1389     """Adjust the specified labels in the syslinux config file src with
1390     specified replacement
1391     """
1392     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1393     for line in fileinput.input(src, inplace=1):
1394         line = label_re.sub(replacement, line)
1395         sys.stdout.write(line)
1396     fileinput.close()
1397
1398
1399 def add_syslinux_entry(filename, grml_flavour):
1400     """Add includes for a specific grml_flavour to the specified filename
1401
1402     @filename: syslinux config file
1403     @grml_flavour: grml flavour to add
1404     """
1405
1406     entry_filename = "option_%s.cfg" % grml_flavour
1407     entry = "include %s\n" % entry_filename
1408
1409     add_entry_if_not_present(filename, entry)
1410     path = os.path.dirname(filename)
1411
1412     data = open(path + "/" + entry_filename, "w")
1413     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1414     data.close()
1415
1416
1417 def modify_filenames(grml_flavour, target, filenames):
1418     """Replace the standard filenames with the new ones
1419
1420     @grml_flavour: grml-flavour strin
1421     @target: directory where the files are located
1422     @filenames: list of filenames to alter
1423     """
1424     grml_filename = get_flavour_filename(grml_flavour)
1425     for filename in filenames:
1426         old_filename = "%s/%s" % (target, filename)
1427         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1428         os.rename(old_filename, new_filename)
1429
1430
1431 def remove_default_entry(filename):
1432     """Remove the default entry from specified syslinux file
1433
1434     @filename: syslinux config file
1435     """
1436     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1437     for line in fileinput.input(filename, inplace=1):
1438         if default_re.match(line):
1439             continue
1440         sys.stdout.write(line)
1441     fileinput.close()
1442
1443
1444 def handle_syslinux_config(grml_flavour, target):
1445     """Main handler for generating syslinux configuration
1446
1447     @grml_flavour: name of grml flavour the configuration should be generated for
1448     @target: path of syslinux's configuration files"""
1449
1450     logging.debug("Generating syslinux configuration")
1451     syslinux_target = target + '/boot/syslinux/'
1452     # should be present via  copy_bootloader_files(), but make sure it exits:
1453     execute(mkdir, syslinux_target)
1454     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1455
1456     # install main configuration only *once*, no matter how many ISOs we have:
1457     syslinux_config_file = open(syslinux_cfg, 'w')
1458     syslinux_config_file.write("timeout 300\n")
1459     syslinux_config_file.write("include vesamenu.cfg\n")
1460     syslinux_config_file.close()
1461
1462     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1463     prompt_name.write('menu label S^yslinux prompt\n')
1464     prompt_name.close()
1465
1466     initial_syslinux_config(syslinux_target)
1467     flavour_filename = get_flavour_filename(grml_flavour)
1468
1469     if search_file('default.cfg', syslinux_target):
1470         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1471
1472     filename = search_file("new_hidden.cfg", syslinux_target)
1473
1474     # process hidden file
1475     if not search_file("hidden.cfg", syslinux_target):
1476         new_hidden = syslinux_target + "hidden.cfg"
1477         os.rename(filename, new_hidden)
1478         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1479     else:
1480         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1481         os.rename(filename, new_hidden_file)
1482         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1483         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1484         entry = 'include %s_hidden.cfg\n' % flavour_filename
1485         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1486
1487     new_default = "%s_default.cfg" % (flavour_filename)
1488     entry = 'include %s\n' % new_default
1489     defaults_file = '%s/defaults.cfg' % syslinux_target
1490     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1491     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1492
1493     if os.path.isfile(defaults_file):
1494
1495         # remove default menu entry in menu
1496         remove_default_entry(new_default_with_path)
1497
1498         # adjust all labels for additional isos
1499         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1500         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1501
1502     # always adjust bootoptions
1503     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1504     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1505
1506     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1507
1508     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1509
1510
1511 def handle_bootloader_config(grml_flavour, device, target):
1512     """Main handler for generating bootloader's configuration
1513
1514     @grml_flavour: name of grml flavour the configuration should be generated for
1515     @device: device/partition where bootloader should be installed to
1516     @target: path of bootloader's configuration files"""
1517
1518     global UUID
1519     UUID = get_uuid(target)
1520     if options.skipsyslinuxconfig:
1521         logging.info("Skipping generation of syslinux configuration as requested.")
1522     else:
1523         try:
1524             handle_syslinux_config(grml_flavour, target)
1525         except CriticalException as error:
1526             logging.critical("Fatal: %s", error)
1527             sys.exit(1)
1528
1529     if options.skipgrubconfig:
1530         logging.info("Skipping generation of grub configuration as requested.")
1531     else:
1532         try:
1533             handle_grub_config(grml_flavour, device, target)
1534         except CriticalException as error:
1535             logging.critical("Fatal: %s", error)
1536             sys.exit(1)
1537
1538
1539 def install(image, device):
1540     """Install a grml image to the specified device
1541
1542     @image: directory or is file
1543     @device: partition or directory to install the device
1544     """
1545     iso_mountpoint = image
1546     remove_image_mountpoint = False
1547     if os.path.isdir(image):
1548         if options.force or os.path.exists(os.path.join(image, 'live')):
1549             logging.info("Using %s as install base", image)
1550         else:
1551             q = raw_input("%s does not look like a Grml system. "
1552                 "Do you really want to use this image? y/N " % image)
1553             if q.lower() == 'y':
1554                 logging.info("Using %s as install base", image)
1555             else:
1556                 logging.info("Skipping install base %s", image)
1557     else:
1558         logging.info("Using ISO %s", image)
1559         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1560         register_tmpfile(iso_mountpoint)
1561         remove_image_mountpoint = True
1562         try:
1563             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1564         except CriticalException as error:
1565             logging.critical("Fatal: %s", error)
1566             sys.exit(1)
1567
1568     try:
1569         install_grml(iso_mountpoint, device)
1570     finally:
1571         if remove_image_mountpoint:
1572             try:
1573                 remove_mountpoint(iso_mountpoint)
1574             except CriticalException as error:
1575                 cleanup()
1576                 raise
1577
1578
1579 def install_grml(mountpoint, device):
1580     """Main logic for copying files of the currently running Grml system.
1581
1582     @mountpoint: directory where currently running live system resides (usually /lib/live/mount/medium)
1583     @device: partition where the specified ISO should be installed to"""
1584
1585     device_mountpoint = device
1586     if os.path.isdir(device):
1587         logging.info("Specified device is a directory, therefore not mounting.")
1588         remove_device_mountpoint = False
1589     else:
1590         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1591         register_tmpfile(device_mountpoint)
1592         remove_device_mountpoint = True
1593         try:
1594             check_for_fat(device)
1595             if not options.skipbootflag:
1596                 check_boot_flag(device)
1597
1598             set_rw(device)
1599             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1600         except CriticalException as error:
1601             mount(device, device_mountpoint, "")
1602     try:
1603         grml_flavours = identify_grml_flavour(mountpoint)
1604         for flavour in set(grml_flavours):
1605             if not flavour:
1606                 logging.warning("No valid flavour found, please check your iso")
1607             logging.info("Identified grml flavour \"%s\".", flavour)
1608             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1609             GRML_FLAVOURS.add(flavour)
1610     finally:
1611         if remove_device_mountpoint:
1612             remove_mountpoint(device_mountpoint)
1613
1614
1615 def remove_mountpoint(mountpoint):
1616     """remove a registered mountpoint
1617     """
1618
1619     try:
1620         unmount(mountpoint, "")
1621         if os.path.isdir(mountpoint):
1622             os.rmdir(mountpoint)
1623             unregister_tmpfile(mountpoint)
1624     except CriticalException as error:
1625         cleanup()
1626         raise
1627
1628
1629 def handle_mbr(device):
1630     """Main handler for installing master boot record (MBR)
1631
1632     @device: device where the MBR should be installed to"""
1633
1634     if options.dryrun:
1635         logging.info("Would install MBR")
1636         return 0
1637
1638     mbr_device, partition_number = get_device_from_partition(device)
1639     if partition_number is None:
1640         logging.warn("Could not detect partition number, not activating partition")
1641
1642     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1643     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1644     if mbr_device == "/dev/loop":
1645         mbr_device = device
1646         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1647
1648     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1649     if options.syslinuxmbr:
1650         mbrcode = ""
1651         mbr_locations = ('/usr/lib/syslinux/mbr.bin',
1652                          '/usr/lib/syslinux/bios/mbr.bin',
1653                          '/usr/share/syslinux/mbr.bin')
1654         for mbrpath in mbr_locations:
1655             if os.path.isfile(mbrpath):
1656                 mbrcode = mbrpath
1657                 break
1658
1659         if mbrcode is "":
1660             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1661             logging.error('Cannot find syslinux MBR, install it at %s)',
1662                           str_locations)
1663             raise CriticalException("syslinux MBR  can not be found at %s."
1664                                     % str_locations)
1665     elif options.mbrmenu:
1666         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1667
1668     try:
1669         install_mbr(mbrcode, mbr_device, partition_number, True)
1670     except (IOError, Exception) as error:
1671         logging.critical("Execution failed: %s", error)
1672         sys.exit(1)
1673
1674
1675 def handle_vfat(device):
1676     """Check for FAT specific settings and options
1677
1678     @device: device that should checked / formated"""
1679
1680     # make sure we have mkfs.vfat available
1681     if options.fat16:
1682         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1683             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1684             logging.critical('Please make sure to install dosfstools.')
1685             sys.exit(1)
1686
1687         if options.force:
1688             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1689         else:
1690             # make sure the user is aware of what he is doing
1691             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1692             if f == "y" or f == "Y":
1693                 logging.info("Note: you can skip this question using the option --force")
1694             else:
1695                 sys.exit(1)
1696         try:
1697             mkfs_fat16(device)
1698         except CriticalException as error:
1699             logging.critical("Execution failed: %s", error)
1700             sys.exit(1)
1701
1702     # check for vfat filesystem
1703     if device is not None and not os.path.isdir(device) and options.syslinux:
1704         try:
1705             check_for_fat(device)
1706         except CriticalException as error:
1707             logging.critical("Execution failed: %s", error)
1708             sys.exit(1)
1709
1710     if options.skipusbcheck:
1711         logging.info("Not checking for removable USB device as requested via option --skip-usb-check.")
1712         return
1713
1714     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1715         print("Warning: the specified device %s does not look like a removable usb device." % device)
1716         f = raw_input("Do you really want to continue? y/N ")
1717         if f.lower() != "y":
1718             sys.exit(1)
1719
1720
1721 def handle_compat_warning(device):
1722     """Backwards compatible checks
1723
1724     @device: device that should be checked"""
1725
1726     # make sure we can replace old grml2usb script and warn user when using old way of life:
1727     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1728         print("Warning: the semantics of grml2usb has changed.")
1729         print("Instead of using grml2usb /path/to/iso %s you might" % device)
1730         print("want to use grml2usb /path/to/iso /dev/... instead.")
1731         print("Please check out the grml2usb manpage for details.")
1732         f = raw_input("Do you really want to continue? y/N ")
1733         if f.lower() != "y":
1734             sys.exit(1)
1735
1736
1737 def handle_logging():
1738     """Log handling and configuration"""
1739
1740     if options.verbose and options.quiet:
1741         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1742
1743     FORMAT = "%(message)s"
1744     if options.verbose:
1745         FORMAT = "%(asctime)-15s %(message)s"
1746         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1747     elif options.quiet:
1748         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1749     else:
1750         logging.basicConfig(level=logging.INFO, format=FORMAT)
1751
1752
1753 def handle_bootloader(device):
1754     """wrapper for installing bootloader
1755
1756     @device: device where bootloader should be installed to"""
1757
1758     # Install bootloader only if not using the --copy-only option
1759     if options.copyonly:
1760         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1761     elif os.path.isdir(device):
1762         logging.info("Not installing bootloader as %s is a directory.", device)
1763     else:
1764         install_bootloader(device)
1765
1766
1767 def check_options(opts):
1768     """Check compability of provided user opts
1769
1770     @opts option dict from OptionParser
1771     """
1772     if opts.grubmbr and not opts.grub:
1773         raise CriticalException("--grub-mbr requires --grub option.")
1774
1775     if opts.copyonly and opts.grub:
1776         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
1777
1778
1779 def check_programs():
1780     """check if all needed programs are installed"""
1781     if options.grub:
1782         global GRUB_INSTALL
1783         GRUB_INSTALL = which("grub-install") or which("grub2-install")
1784         if not GRUB_INSTALL:
1785             logging.critical("Fatal: grub-install not available (please install the " +
1786                              "grub package or drop the --grub option)")
1787             sys.exit(1)
1788
1789     if options.syslinux:
1790         if not which("syslinux"):
1791             logging.critical("Fatal: syslinux not available (please install the " +
1792                              "syslinux package or use the --grub option)")
1793             sys.exit(1)
1794
1795     if not which("rsync"):
1796         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1797         sys.exit(1)
1798
1799
1800 def load_loop():
1801     """Runs modprobe loop and throws away it's output"""
1802     if not which("modprobe"):
1803         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
1804         logging.critical("Hint: is /sbin missing in PATH?")
1805         sys.exit(1)
1806
1807     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1808     proc.wait()
1809
1810
1811 def main():
1812     """Main function [make pylint happy :)]"""
1813
1814     try:
1815         if options.version:
1816             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
1817             sys.exit(0)
1818
1819         if len(args) < 2:
1820             parser.error("invalid usage")
1821
1822         # log handling
1823         handle_logging()
1824
1825         # make sure we have the appropriate permissions
1826         check_uid_root()
1827
1828         check_options(options)
1829
1830         load_loop()
1831
1832         logging.info("Executing grml2usb version %s", PROG_VERSION)
1833
1834         if options.dryrun:
1835             logging.info("Running in simulation mode as requested via option dry-run.")
1836
1837         check_programs()
1838
1839         # specified arguments
1840         device = os.path.realpath(args[len(args) - 1])
1841         isos = args[0:len(args) - 1]
1842
1843         if not os.path.isdir(device):
1844             if device[-1:].isdigit():
1845                 if int(device[-1:]) > 4 or device[-2:].isdigit():
1846                     logging.warn("Warning: installing on partition number >4, booting *might* fail depending on your system.")
1847
1848         # provide upgrade path
1849         handle_compat_warning(device)
1850
1851         # check for vfat partition
1852         handle_vfat(device)
1853
1854         # main operation (like installing files)
1855         for iso in isos:
1856             install(iso, device)
1857
1858         # install mbr
1859         is_superfloppy = not device[-1:].isdigit()
1860         if is_superfloppy:
1861             logging.info("Detected superfloppy format - not installing MBR")
1862
1863         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1864             handle_mbr(device)
1865
1866         handle_bootloader(device)
1867
1868         logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1869
1870         for flavour in GRML_FLAVOURS:
1871             logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1872
1873         # finally be polite :)
1874         logging.info("Finished execution of grml2usb (%s). Have fun with your Grml system.", PROG_VERSION)
1875
1876     except Exception as error:
1877         logging.critical("Fatal: %s", str(error))
1878         sys.exit(1)
1879
1880
1881 if __name__ == "__main__":
1882     try:
1883         main()
1884     except KeyboardInterrupt:
1885         logging.info("Received KeyboardInterrupt")
1886         cleanup()
1887
1888 # END OF FILE ##################################################################
1889 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8