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