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