Bugfixes.
[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 exception 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.error('Error installing MBR (either try --syslinux-mbr or '
542             'install missing file "%s"?)', mbrtemplate)
543         raise CriticalException("Error: %s can not be read.", mbrtemplate)
544
545     if partition is not None and ((partition < 0) or (partition > 3)):
546         logging.warn("Cannot activate partition %d", partition)
547         partition = None
548
549     if ismirbsdmbr:
550         nmbrbytes = 439
551     else:
552         nmbrbytes = 440
553
554     tmpf = tempfile.NamedTemporaryFile()
555
556     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
557     proc = subprocess.Popen(["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
558                             stderr=file(os.devnull, "r+"))
559     proc.wait()
560     if proc.returncode != 0:
561         raise Exception("error executing dd (first run)")
562
563     logging.debug("executing: dd if=%s of=%s bs=%s count=1 conv=notrunc", mbrtemplate,
564                   tmpf.name, nmbrbytes)
565     proc = subprocess.Popen(["dd", "if=%s" % mbrtemplate, "of=%s" % tmpf.name, "bs=%s" % nmbrbytes,
566                              "count=1", "conv=notrunc"], stderr=file(os.devnull, "r+"))
567     proc.wait()
568     if proc.returncode != 0:
569         raise Exception("error executing dd (second run)")
570
571     mbrcode = tmpf.file.read(512)
572     if len(mbrcode) < 512:
573         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
574
575     if partition is not None:
576         if ismirbsdmbr:
577             mbrcode = mbrcode[0:439] + chr(partition) + \
578                     mbrcode[440:510] + "\x55\xAA"
579         else:
580             actives = ["\x00", "\x00", "\x00", "\x00"]
581             actives[partition] = "\x80"
582             mbrcode = mbrcode[0:446] + actives[0] + \
583                     mbrcode[447:462] + actives[1] + \
584                     mbrcode[463:478] + actives[2] + \
585                     mbrcode[479:494] + actives[3] + \
586                     mbrcode[495:510] + "\x55\xAA"
587
588     tmpf.file.seek(0)
589     tmpf.file.truncate()
590     tmpf.file.write(mbrcode)
591     tmpf.file.close()
592
593     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device)
594     proc = subprocess.Popen(["dd", "if=%s" % tmpf.name, "of=%s" % device, "bs=512", "count=1",
595                              "conv=notrunc"], stderr=file(os.devnull, "r+"))
596     proc.wait()
597     if proc.returncode != 0:
598         raise Exception("error executing dd (third run)")
599     del tmpf
600
601
602 def is_writeable(device):
603     """Check if the device is writeable for the current user
604
605     @device: partition where bootloader should be installed to"""
606
607     if not device:
608         return False
609         #raise Exception("no device for checking write permissions")
610
611     if not os.path.exists(device):
612         return False
613
614     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
615
616
617 def mount(source, target, mount_options):
618     """Mount specified source on given target
619
620     @source: name of device/ISO that should be mounted
621     @target: directory where the ISO should be mounted to
622     @options: mount specific options"""
623
624     # note: options.dryrun does not work here, as we have to
625     # locate files and identify the grml flavour
626
627     for x in file('/proc/mounts').readlines():
628         if x.startswith(source):
629             raise CriticalException("Error executing mount: %s already mounted - " % source
630                                     + "please unmount before invoking grml2usb")
631
632     if os.path.isdir(source):
633         logging.debug("Source %s is not a device, therefore not mounting.", source)
634         return 0
635
636     logging.debug("mount %s %s %s", mount_options, source, target)
637     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
638     proc.wait()
639     if proc.returncode != 0:
640         raise CriticalException("Error executing mount (no filesystem on the partition?)")
641     else:
642         logging.debug("register_mountpoint(%s)", target)
643         register_mountpoint(target)
644
645
646 def unmount(target, unmount_options):
647     """Unmount specified target
648
649     @target: target where something is mounted on and which should be unmounted
650     @options: options for umount command"""
651
652     # make sure we unmount only already mounted targets
653     target_unmount = False
654     mounts = open('/proc/mounts').readlines()
655     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
656     for line in mounts:
657         if re.match(mountstring, line):
658             target_unmount = True
659
660     if not target_unmount:
661         logging.debug("%s not mounted anymore", target)
662     else:
663         logging.debug("umount %s %s", list(unmount_options), target)
664         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
665         proc.wait()
666         if proc.returncode != 0:
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.error("error locating squashfs file")
799         raise CriticalException("squashfs file not found"
800             ", please check that your iso is not corrupt")
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.error("error locating filesystem.module file")
812         raise CriticalException("filesystem.module not found")
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.error("error locating kernel file")
827             raise CriticalException("Kernel not found")
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         raise CriticalException(
1051             "file default.cfg could not be found.\n"
1052             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1053             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
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             raise
1168         finally:
1169             if tmpfile:
1170                 tmpfile.close()
1171
1172     return flavours
1173
1174
1175 def get_bootoptions(grml_flavour):
1176     """Returns bootoptions for specific flavour
1177
1178     @grml_flavour: name of the grml_flavour
1179     """
1180     # do NOT write "None" in kernel cmdline
1181     if not options.bootoptions:
1182         bootopt = ""
1183     else:
1184         bootopt = " ".join(options.bootoptions)
1185     bootopt = bootopt.replace("%flavour", grml_flavour)
1186     return bootopt
1187
1188
1189 def handle_grub_config(grml_flavour, device, target):
1190     """Main handler for generating grub (v1 and v2) configuration
1191
1192     @grml_flavour: name of grml flavour the configuration should be generated for
1193     @device: device/partition where grub should be installed to
1194     @target: path of grub's configuration files"""
1195
1196     global UUID
1197
1198     logging.debug("Updating grub configuration")
1199
1200     grub_target = target + '/boot/grub/'
1201
1202     bootid_re = re.compile("bootid=[\w_-]+")
1203     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1204
1205     bootopt = get_bootoptions(grml_flavour)
1206
1207     remove_regexes = []
1208     option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1209
1210     if options.removeoption:
1211         for regex in options.removeoption:
1212             remove_regexes.append(re.compile(regex))
1213
1214     shortname = get_shortname(grml_flavour)
1215     for filename in glob.glob(grub_target + '*.cfg'):
1216         for line in fileinput.input(filename, inplace=1):
1217             line = line.rstrip("\r\n")
1218             if option_re.search(line):
1219                 line = bootid_re.sub('', line)
1220                 if shortname in filename:
1221                     line = live_media_path_re.sub('', line)
1222                     line = line.rstrip() + ' live-media-path=/live/%s/ ' % (grml_flavour)
1223                 line = line.rstrip() + r' bootid=%s %s ' % (UUID, bootopt)
1224                 for regex in remove_regexes:
1225                     line = regex.sub(' ', line)
1226             print line
1227         fileinput.close()
1228
1229
1230 def initial_syslinux_config(target):
1231     """Generates intial syslinux configuration
1232
1233     @target path of syslinux's configuration files"""
1234
1235     target = target + "/"
1236     filename = target + "grmlmain.cfg"
1237     if os.path.isfile(target + "grmlmain.cfg"):
1238         return
1239     data = open(filename, "w")
1240     data.write(generate_main_syslinux_config())
1241     data.close()
1242
1243     filename = target + "hiddens.cfg"
1244     data = open(filename, "w")
1245     data.write("include hidden.cfg\n")
1246     data.close()
1247
1248
1249 def add_entry_if_not_present(filename, entry):
1250     """Write entry into filename if entry is not already in the file
1251
1252     @filanme: name of the file
1253     @entry: data to write to the file
1254     """
1255     data = open(filename, "a+")
1256     for line in data:
1257         if line == entry:
1258             break
1259     else:
1260         data.write(entry)
1261
1262     data.close()
1263
1264
1265 def get_flavour_filename(flavour):
1266     """Generate a iso9960 save filename out of the specified flavour
1267
1268     @flavour: grml flavour
1269     """
1270     return flavour.replace('-', '_')
1271
1272
1273 def adjust_syslinux_bootoptions(src, flavour):
1274     """Adjust existing bootoptions of specified syslinux config to
1275     grml2usb specific ones, e.g. change the location of the kernel...
1276
1277     @src: config file to alter
1278     @flavour: grml flavour
1279     """
1280
1281     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1282     # flavour_re = re.compile("(label.*)(grml\w+)")
1283     default_re = re.compile("(default.cfg)")
1284     bootid_re = re.compile("bootid=[\w_-]+")
1285     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1286
1287     bootopt = get_bootoptions(flavour)
1288
1289     regexe = []
1290     option_re = None
1291     if options.removeoption:
1292         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1293
1294         for regex in options.removeoption:
1295             regexe.append(re.compile(r'%s' % regex))
1296
1297     for line in fileinput.input(src, inplace=1):
1298         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1299         line = default_re.sub(r'%s-\1' % flavour, line)
1300         line = bootid_re.sub('', line)
1301         line = live_media_path_re.sub('', line)
1302         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1303         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1304         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1305         if option_re and option_re.search(line):
1306             for regex in regexe:
1307                 line = regex.sub(' ', line)
1308         sys.stdout.write(line)
1309     fileinput.close()
1310
1311
1312 def adjust_labels(src, replacement):
1313     """Adjust the specified labels in the syslinux config file src with
1314     specified replacement
1315     """
1316     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1317     for line in fileinput.input(src, inplace=1):
1318         line = label_re.sub(replacement, line)
1319         sys.stdout.write(line)
1320     fileinput.close()
1321
1322
1323 def add_syslinux_entry(filename, grml_flavour):
1324     """Add includes for a specific grml_flavour to the specified filename
1325
1326     @filename: syslinux config file
1327     @grml_flavour: grml flavour to add
1328     """
1329
1330     entry_filename = "option_%s.cfg" % grml_flavour
1331     entry = "include %s\n" % entry_filename
1332
1333     add_entry_if_not_present(filename, entry)
1334     path = os.path.dirname(filename)
1335
1336     data = open(path + "/" + entry_filename, "w")
1337     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1338     data.close()
1339
1340
1341 def modify_filenames(grml_flavour, target, filenames):
1342     """Replace the standard filenames with the new ones
1343
1344     @grml_flavour: grml-flavour strin
1345     @target: directory where the files are located
1346     @filenames: list of filenames to alter
1347     """
1348     grml_filename = get_flavour_filename(grml_flavour)
1349     for filename in filenames:
1350         old_filename = "%s/%s" % (target, filename)
1351         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1352         os.rename(old_filename, new_filename)
1353
1354
1355 def remove_default_entry(filename):
1356     """Remove the default entry from specified syslinux file
1357
1358     @filename: syslinux config file
1359     """
1360     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1361     for line in fileinput.input(filename, inplace=1):
1362         if default_re.match(line):
1363             continue
1364         sys.stdout.write(line)
1365     fileinput.close()
1366
1367
1368 def handle_syslinux_config(grml_flavour, target):
1369     """Main handler for generating syslinux configuration
1370
1371     @grml_flavour: name of grml flavour the configuration should be generated for
1372     @target: path of syslinux's configuration files"""
1373
1374     logging.debug("Generating syslinux configuration")
1375     syslinux_target = target + '/boot/syslinux/'
1376     # should be present via  copy_bootloader_files(), but make sure it exits:
1377     execute(mkdir, syslinux_target)
1378     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1379
1380     # install main configuration only *once*, no matter how many ISOs we have:
1381     syslinux_config_file = open(syslinux_cfg, 'w')
1382     syslinux_config_file.write("TIMEOUT 300\n")
1383     syslinux_config_file.write("include vesamenu.cfg\n")
1384     syslinux_config_file.close()
1385
1386     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1387     prompt_name.write('menu label S^yslinux prompt\n')
1388     prompt_name.close()
1389
1390     initial_syslinux_config(syslinux_target)
1391     flavour_filename = get_flavour_filename(grml_flavour)
1392
1393     if search_file('default.cfg', syslinux_target):
1394         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1395
1396     filename = search_file("new_hidden.cfg", syslinux_target)
1397
1398     # process hidden file
1399     if not search_file("hidden.cfg", syslinux_target):
1400         new_hidden = syslinux_target + "hidden.cfg"
1401         os.rename(filename, new_hidden)
1402         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1403     else:
1404         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1405         os.rename(filename, new_hidden_file)
1406         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1407         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1408         entry = 'include %s_hidden.cfg\n' % flavour_filename
1409         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1410
1411     new_default = "%s_default.cfg" % (flavour_filename)
1412     entry = 'include %s\n' % new_default
1413     defaults_file = '%s/defaults.cfg' % syslinux_target
1414     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1415     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1416
1417     if os.path.isfile(defaults_file):
1418
1419         # remove default menu entry in menu
1420         remove_default_entry(new_default_with_path)
1421
1422         # adjust all labels for additional isos
1423         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1424         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1425
1426     # always adjust bootoptions
1427     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1428     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1429
1430     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1431
1432     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1433
1434
1435 def handle_bootloader_config(grml_flavour, device, target):
1436     """Main handler for generating bootloader's configuration
1437
1438     @grml_flavour: name of grml flavour the configuration should be generated for
1439     @device: device/partition where bootloader should be installed to
1440     @target: path of bootloader's configuration files"""
1441
1442     global UUID
1443     UUID = get_uuid(target)
1444     if options.skipsyslinuxconfig:
1445         logging.info("Skipping generation of syslinux configuration as requested.")
1446     else:
1447         try:
1448             handle_syslinux_config(grml_flavour, target)
1449         except CriticalException, error:
1450             logging.critical("Fatal: %s", error)
1451             sys.exit(1)
1452
1453     if options.skipgrubconfig:
1454         logging.info("Skipping generation of grub configuration as requested.")
1455     else:
1456         try:
1457             handle_grub_config(grml_flavour, device, target)
1458         except CriticalException, error:
1459             logging.critical("Fatal: %s", error)
1460             sys.exit(1)
1461
1462
1463 def install(image, device):
1464     """Install a grml image to the specified device
1465
1466     @image: directory or is file
1467     @device: partition or directory to install the device
1468     """
1469     iso_mountpoint = image
1470     remove_image_mountpoint = False
1471     if os.path.isdir(image):
1472         logging.info("Using %s as install base", image)
1473     else:
1474         logging.info("Using ISO %s", image)
1475         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1476         register_tmpfile(iso_mountpoint)
1477         remove_image_mountpoint = True
1478         try:
1479             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1480         except CriticalException, error:
1481             logging.critical("Fatal: %s", error)
1482             sys.exit(1)
1483
1484     try:
1485         install_grml(iso_mountpoint, device)
1486     finally:
1487         if remove_image_mountpoint:
1488             try:
1489                 remove_mountpoint(iso_mountpoint)
1490             except CriticalException, error:
1491                 cleanup()
1492                 raise
1493
1494
1495 def install_grml(mountpoint, device):
1496     """Main logic for copying files of the currently running grml system.
1497
1498     @mountpoint: directory where currently running live system resides (usually /lib/live/mount/medium)
1499     @device: partition where the specified ISO should be installed to"""
1500
1501     device_mountpoint = device
1502     if os.path.isdir(device):
1503         logging.info("Specified device is a directory, therefore not mounting.")
1504         remove_device_mountpoint = False
1505     else:
1506         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1507         register_tmpfile(device_mountpoint)
1508         remove_device_mountpoint = True
1509         try:
1510             check_for_fat(device)
1511             check_boot_flag(device)
1512             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1513         except VerifyException, error:
1514             raise
1515         except CriticalException, error:
1516             mount(device, device_mountpoint, "")
1517     try:
1518         grml_flavours = identify_grml_flavour(mountpoint)
1519         for flavour in set(grml_flavours):
1520             if not flavour:
1521                 logging.warning("No valid flavour found, please check your iso")
1522             logging.info("Identified grml flavour \"%s\".", flavour)
1523             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1524             GRML_FLAVOURS.add(flavour)
1525     finally:
1526         if remove_device_mountpoint:
1527             remove_mountpoint(device_mountpoint)
1528
1529
1530 def remove_mountpoint(mountpoint):
1531     """remove a registered mountpoint
1532     """
1533
1534     try:
1535         unmount(mountpoint, "")
1536         if os.path.isdir(mountpoint):
1537             os.rmdir(mountpoint)
1538             unregister_tmpfile(mountpoint)
1539     except CriticalException, error:
1540         cleanup()
1541         raise
1542
1543
1544 def handle_mbr(device):
1545     """Main handler for installing master boot record (MBR)
1546
1547     @device: device where the MBR should be installed to"""
1548
1549     if options.dryrun:
1550         logging.info("Would install MBR")
1551         return 0
1552
1553     mbr_device, partition_number = get_device_from_partition(device)
1554     if partition_number is None:
1555         logging.warn("Could not detect partition number, not activating partition")
1556
1557     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1558     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1559     if mbr_device == "/dev/loop":
1560         mbr_device = device
1561         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1562
1563     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1564     if options.syslinuxmbr:
1565         mbrcode = '/usr/lib/syslinux/mbr.bin'
1566     elif options.mbrmenu:
1567         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1568
1569     try:
1570         install_mbr(mbrcode, mbr_device, partition_number, True)
1571     except IOError, error:
1572         logging.critical("Execution failed: %s", error)
1573         sys.exit(1)
1574     except Exception, error:
1575         logging.critical("Execution failed: %s", error)
1576         sys.exit(1)
1577
1578
1579 def handle_vfat(device):
1580     """Check for FAT specific settings and options
1581
1582     @device: device that should checked / formated"""
1583
1584     # make sure we have mkfs.vfat available
1585     if options.fat16:
1586         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1587             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1588             logging.critical('Please make sure to install dosfstools.')
1589             sys.exit(1)
1590
1591         if options.force:
1592             print "Forcing mkfs.fat16 on %s as requested via option --force." % device
1593         else:
1594             # make sure the user is aware of what he is doing
1595             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1596             if f == "y" or f == "Y":
1597                 logging.info("Note: you can skip this question using the option --force")
1598             else:
1599                 sys.exit(1)
1600         try:
1601             mkfs_fat16(device)
1602         except CriticalException, error:
1603             logging.critical("Execution failed: %s", error)
1604             sys.exit(1)
1605
1606     # check for vfat filesystem
1607     if device is not None and not os.path.isdir(device) and options.syslinux:
1608         try:
1609             check_for_fat(device)
1610         except CriticalException, error:
1611             logging.critical("Execution failed: %s", error)
1612             sys.exit(1)
1613
1614     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1615         print "Warning: the specified device %s does not look like a removable usb device." % device
1616         f = raw_input("Do you really want to continue? y/N ")
1617         if f == "y" or f == "Y":
1618             pass
1619         else:
1620             sys.exit(1)
1621
1622
1623 def handle_compat_warning(device):
1624     """Backwards compatible checks
1625
1626     @device: device that should be checked"""
1627
1628     # make sure we can replace old grml2usb script and warn user when using old way of life:
1629     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1630         print "Warning: the semantics of grml2usb has changed."
1631         print "Instead of using grml2usb /path/to/iso %s you might" % device
1632         print "want to use grml2usb /path/to/iso /dev/... instead."
1633         print "Please check out the grml2usb manpage for details."
1634         f = raw_input("Do you really want to continue? y/N ")
1635         if f == "y" or f == "Y":
1636             pass
1637         else:
1638             sys.exit(1)
1639
1640
1641 def handle_logging():
1642     """Log handling and configuration"""
1643
1644     if options.verbose and options.quiet:
1645         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1646
1647     FORMAT = "%(message)s"
1648     if options.verbose:
1649         FORMAT = "%(asctime)-15s %(message)s"
1650         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1651     elif options.quiet:
1652         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1653     else:
1654         logging.basicConfig(level=logging.INFO, format=FORMAT)
1655
1656
1657 def handle_bootloader(device):
1658     """wrapper for installing bootloader
1659
1660     @device: device where bootloader should be installed to"""
1661
1662     # Install bootloader only if not using the --copy-only option
1663     if options.copyonly:
1664         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1665     elif os.path.isdir(device):
1666         logging.info("Not installing bootloader as %s is a directory.", device)
1667     else:
1668         install_bootloader(device)
1669
1670
1671 def check_options(opts):
1672     """Check compability of provided user opts
1673
1674     @opts option dict from OptionParser
1675     """
1676     if opts.grubmbr and not opts.grub:
1677         logging.critical("Error: --grub-mbr requires --grub option.")
1678         sys.exit(1)
1679
1680
1681 def check_programs():
1682     """check if all needed programs are installed"""
1683     if options.grub:
1684         if not which("grub-install"):
1685             logging.critical("Fatal: grub-install not available (please install the "
1686                              + "grub package or drop the --grub option)")
1687             sys.exit(1)
1688
1689     if options.syslinux:
1690         if not which("syslinux"):
1691             logging.critical("Fatal: syslinux not available (please install the "
1692                              + "syslinux package or use the --grub option)")
1693             sys.exit(1)
1694
1695     if not which("rsync"):
1696         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1697         sys.exit(1)
1698
1699
1700 def load_loop():
1701     """Runs modprobe loop and throws away it's output"""
1702     if not which("modprobe"):
1703         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
1704         logging.critical("Hint: is /sbin missing in PATH?")
1705         sys.exit(1)
1706
1707     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1708     proc.wait()
1709
1710
1711 def main():
1712     """Main function [make pylint happy :)]"""
1713
1714     try:
1715         if options.version:
1716             print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
1717             sys.exit(0)
1718
1719         if len(args) < 2:
1720             parser.error("invalid usage")
1721
1722         # log handling
1723         handle_logging()
1724
1725         # make sure we have the appropriate permissions
1726         check_uid_root()
1727
1728         check_options(options)
1729
1730         load_loop()
1731
1732         logging.info("Executing grml2usb version %s", PROG_VERSION)
1733
1734         if options.dryrun:
1735             logging.info("Running in simulation mode as requested via option dry-run.")
1736
1737         check_programs()
1738
1739         # specified arguments
1740         device = os.path.realpath(args[len(args) - 1])
1741         isos = args[0:len(args) - 1]
1742
1743         if not os.path.isdir(device):
1744             if device[-1:].isdigit():
1745                 if int(device[-1:]) > 4 or device[-2:].isdigit():
1746                     logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1747                     sys.exit(1)
1748
1749         # provide upgrade path
1750         handle_compat_warning(device)
1751
1752         # check for vfat partition
1753         handle_vfat(device)
1754
1755         # main operation (like installing files)
1756         for iso in isos:
1757             install(iso, device)
1758
1759         # install mbr
1760         is_superfloppy = not device[-1:].isdigit()
1761         if is_superfloppy:
1762             logging.info("Detected superfloppy format - not installing MBR")
1763
1764         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1765             handle_mbr(device)
1766
1767         handle_bootloader(device)
1768
1769         logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1770
1771         for flavour in GRML_FLAVOURS:
1772             logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1773
1774         # finally be politely :)
1775         logging.info("Finished execution of grml2usb (%s). Have fun with your grml system.", PROG_VERSION)
1776
1777     except Exception, error:
1778         logging.critical("Fatal: %s", str(error))
1779         sys.exit(1)
1780
1781
1782 if __name__ == "__main__":
1783     try:
1784         main()
1785     except KeyboardInterrupt:
1786         logging.info("Received KeyboardInterrupt")
1787         cleanup()
1788
1789 ## END OF FILE #################################################################
1790 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8