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