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