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