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