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