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