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