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