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