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