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