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