ce0bfd53f6c9955cef3b9d698f8a985cd1e75bb2
[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-usb-check", dest="skipusbcheck", action="store_true",
111                   help="skip check to verify whether given device is removable")
112 parser.add_option("--skip-grub-config", dest="skipgrubconfig", action="store_true",
113                   help="skip generation of grub configuration files")
114 parser.add_option("--skip-mbr", dest="skipmbr", action="store_true",
115                   help="do not install a master boot record (MBR) on the device")
116 parser.add_option("--skip-syslinux-config", dest="skipsyslinuxconfig", action="store_true",
117                   help="skip generation of syslinux configuration files")
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
649 def is_writeable(device):
650     """Check if the device is writeable for the current user
651
652     @device: partition where bootloader should be installed to"""
653
654     if not device:
655         return False
656         #raise Exception("no device for checking write permissions")
657
658     if not os.path.exists(device):
659         return False
660
661     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
662
663
664 def mount(source, target, mount_options):
665     """Mount specified source on given target
666
667     @source: name of device/ISO that should be mounted
668     @target: directory where the ISO should be mounted to
669     @options: mount specific options"""
670
671     # note: options.dryrun does not work here, as we have to
672     # locate files and identify the grml flavour
673
674     for x in file('/proc/mounts').readlines():
675         if x.startswith(source):
676             raise CriticalException("Error executing mount: %s already mounted - " % source
677                                     + "please unmount before invoking grml2usb")
678
679     if os.path.isdir(source):
680         logging.debug("Source %s is not a device, therefore not mounting.", source)
681         return 0
682
683     logging.debug("mount %s %s %s", mount_options, source, target)
684     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
685     proc.wait()
686     if proc.returncode != 0:
687         raise CriticalException("Error executing mount (no filesystem on the partition?)")
688     else:
689         logging.debug("register_mountpoint(%s)", target)
690         register_mountpoint(target)
691
692
693 def unmount(target, unmount_options):
694     """Unmount specified target
695
696     @target: target where something is mounted on and which should be unmounted
697     @options: options for umount command"""
698
699     # make sure we unmount only already mounted targets
700     target_unmount = False
701     mounts = open('/proc/mounts').readlines()
702     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
703     for line in mounts:
704         if re.match(mountstring, line):
705             target_unmount = True
706
707     if not target_unmount:
708         logging.debug("%s not mounted anymore", target)
709     else:
710         logging.debug("umount %s %s", list(unmount_options), target)
711         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
712         proc.wait()
713         if proc.returncode != 0:
714             raise Exception("Error executing umount")
715         else:
716             logging.debug("unregister_mountpoint(%s)", target)
717             unregister_mountpoint(target)
718
719
720 def check_for_usbdevice(device):
721     """Check whether the specified device is a removable USB device
722
723     @device: device name, like /dev/sda1 or /dev/sda
724     """
725
726     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
727     # newer systems:
728     usbdev = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
729     if not os.path.isfile(usbdev):
730         # Ubuntu with kernel 2.6.24 for example:
731         usbdev = os.path.realpath('/sys/block/' + usbdevice + '/removable')
732
733     if os.path.isfile(usbdev):
734         is_usb = open(usbdev).readline()
735         if is_usb.find("1"):
736             return 0
737
738     return 1
739
740
741 def check_for_fat(partition):
742     """Check whether specified partition is a valid VFAT/FAT16 filesystem
743
744     @partition: device name of partition"""
745
746     if not os.access(partition, os.R_OK):
747         raise CriticalException("Failed to read device %s"
748                 " (wrong UID/permissions or device/directory not present?)" % partition)
749
750     try:
751         udev_info = subprocess.Popen(["/sbin/blkid", "-s", "TYPE", "-o", "value", partition],
752                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
753         filesystem = udev_info.communicate()[0].rstrip()
754
755         if filesystem != "vfat":
756             raise CriticalException(
757                     "Partition %s does not contain a FAT16 filesystem. "
758                     "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition))
759
760     except OSError:
761         raise CriticalException("Sorry, /sbin/blkid not available (install e2fsprogs?)")
762
763
764 def mkdir(directory):
765     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
766
767     # just silently pass as it's just fine it the directory exists
768     if not os.path.isdir(directory):
769         try:
770             os.makedirs(directory)
771         # pylint: disable-msg=W0704
772         except OSError:
773             pass
774
775
776 def exec_rsync(source, target):
777     """Simple wrapper around rsync to install files
778
779     @source: source file/directory
780     @target: target file/directory"""
781     logging.debug("Source: %s / Target: %s", source, target)
782     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
783     proc.wait()
784     if proc.returncode == 12:
785         logging.critical("Fatal: No space left on device")
786         cleanup()
787         sys.exit(1)
788
789     if proc.returncode != 0:
790         logging.critical("Fatal: could not install %s", source)
791         cleanup()
792         sys.exit(1)
793
794
795 def write_uuid(target_file):
796     """Generates an returns uuid and write it to the specified file
797
798     @target_file: filename to write the uuid to
799     """
800
801     fileh = open(target_file, 'w')
802     uid = str(uuid.uuid4())
803     fileh.write(uid)
804     fileh.close()
805     return uid
806
807
808 def get_uuid(target):
809     """Get the uuid of the specified target. Will generate an uuid if none exist.
810
811     @target: directory/mountpoint containing the grml layout
812     """
813
814     conf_target = target + "/conf/"
815     uuid_file_name = conf_target + "/bootid.txt"
816     if os.path.isdir(conf_target):
817         if os.path.isfile(uuid_file_name):
818             uuid_file = open(uuid_file_name, 'r')
819             uid = uuid_file.readline().strip()
820             uuid_file.close()
821             return uid
822         else:
823             return write_uuid(uuid_file_name)
824     else:
825         execute(mkdir, conf_target)
826         return write_uuid(uuid_file_name)
827
828
829 def get_shortname(grml_flavour):
830     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
831     @grml_flavour: flavour name which shold be translated to shortname"""
832
833     return re.sub(r'[,._-]', '', grml_flavour)
834
835
836 def copy_system_files(grml_flavour, iso_mount, target):
837     """copy grml's main files (like squashfs, kernel and initrd) to a given target
838
839     @grml_flavour: name of grml flavour the configuration should be generated for
840     @iso_mount: path where a grml ISO is mounted on
841     @target: path where grml's main files should be copied to"""
842
843     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
844     if squashfs is None:
845         logging.error("error locating squashfs file")
846         raise CriticalException("squashfs file not found, please check that your iso is not corrupt")
847     else:
848         squashfs_target = target + '/live/' + grml_flavour + '/'
849         execute(mkdir, squashfs_target)
850     exec_rsync(squashfs, squashfs_target + grml_flavour + '.squashfs')
851
852     for prefix in grml_flavour + "/", "":
853         filesystem_module = search_file(prefix + 'filesystem.module', iso_mount)
854         if filesystem_module:
855             break
856     if filesystem_module is None:
857         logging.error("error locating filesystem.module file")
858         raise CriticalException("filesystem.module not found")
859     else:
860         exec_rsync(filesystem_module, squashfs_target + 'filesystem.module')
861
862     shortname = get_shortname(grml_flavour)
863     if os.path.isdir(iso_mount + '/boot/' + shortname):
864         exec_rsync(iso_mount + '/boot/' + shortname, target + '/boot')
865     else:
866         kernel = search_file('vmlinuz', iso_mount)
867         if kernel is None:
868             # compat for releases < 2011.12
869             kernel = search_file('linux26', iso_mount)
870
871         if kernel is None:
872             logging.error("error locating kernel file")
873             raise CriticalException("Kernel not found")
874
875         source = os.path.dirname(kernel) + '/'
876         dest = target + '/' + os.path.dirname(kernel).replace(iso_mount, '') + '/'
877         execute(mkdir, dest)
878         exec_rsync(source, dest)
879
880
881 def update_grml_versions(iso_mount, target):
882     """Update the grml version file on a cd
883     Returns true if version was updated successfully,
884     False if grml-version does not exist yet on the mountpoint
885
886     @iso_mount: string of the iso mount point
887     @target: path of the target mount point
888     """
889     grml_target = target + '/grml/'
890     target_grml_version_file = search_file('grml-version', grml_target)
891     if target_grml_version_file:
892         iso_grml_version_file = search_file('grml-version', iso_mount)
893         if not iso_grml_version_file:
894             logging.warn("Warning: %s could not be found - can not install it", iso_grml_version_file)
895             return False
896         try:
897             # read the flavours from the iso image
898             iso_versions = {}
899             iso_file = open(iso_grml_version_file, 'r')
900             for line in iso_file:
901                 iso_versions[get_flavour(line)] = line.strip()
902
903             # update the existing flavours on the target
904             for line in fileinput.input([target_grml_version_file], inplace=1):
905                 flavour = get_flavour(line)
906                 if flavour in iso_versions.keys():
907                     print(iso_versions.pop(flavour))
908                 else:
909                     print(line.strip())
910             fileinput.close()
911
912             target_file = open(target_grml_version_file, 'a')
913             # add the new flavours from the current iso
914             for flavour in iso_versions:
915                 target_file.write("%s\n" % iso_versions[flavour])
916         except IOError:
917             logging.warn("Warning: Could not write file")
918         finally:
919             iso_file.close()
920             target_file.close()
921         return True
922     else:
923         return False
924
925
926 def copy_grml_files(grml_flavour, iso_mount, target):
927     """copy some minor grml files to a given target
928
929     @grml_flavour: the current grml_flavour
930     @iso_mount: path where a grml ISO is mounted on
931     @target: path where grml's main files should be copied to"""
932
933     grml_target = target + '/grml/'
934     execute(mkdir, grml_target)
935
936     grml_prefixe = ["GRML", "grml"]
937     for prefix in grml_prefixe:
938         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
939         if os.path.exists(filename):
940             exec_rsync(filename, grml_target)
941             break
942     else:
943         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
944
945
946 def handle_addon_copy(filename, dst, iso_mount, ignore_errors=False):
947     """handle copy of optional addons
948
949     @filename: filename of the addon
950     @dst: destination directory
951     @iso_mount: location of the iso mount
952     @ignore_errors: don't report missing files
953     """
954     file_location = search_file(filename, iso_mount)
955     if file_location is None:
956         if not ignore_errors:
957             logging.warn("Warning: %s not found (that's fine if you don't need it)", filename)
958     else:
959         exec_rsync(file_location, dst)
960
961
962 def copy_addons(iso_mount, target):
963     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
964
965     @iso_mount: path where a grml ISO is mounted on
966     @target: path where grml's main files should be copied to"""
967
968     addons = target + '/boot/addons/'
969     execute(mkdir, addons)
970
971     # grub all-in-one image
972     handle_addon_copy('allinone.img', addons, iso_mount)
973
974     # bsd image
975     handle_addon_copy('bsd4grml', addons, iso_mount)
976
977     # DOS image
978     handle_addon_copy('balder10.imz', addons, iso_mount)
979
980     # syslinux + pci.ids for hdt
981     for expr in '*.c32', 'pci.ids':
982         glob_and_copy(iso_mount + '/boot/addons/' + expr, addons)
983
984     # memdisk image
985     handle_addon_copy('memdisk', addons, iso_mount)
986
987     # memtest86+ image
988     handle_addon_copy('memtest', addons, iso_mount)
989
990     # gpxe.lkrn: got replaced by ipxe
991     handle_addon_copy('gpxe.lkrn', addons, iso_mount, ignore_errors=True)
992
993     # ipxe.lkrn
994     handle_addon_copy('ipxe.lkrn', addons, iso_mount)
995
996
997 def build_loopbackcfg(target):
998     """Generate GRUB's loopback.cfg based on existing config files.
999
1000     @target: target directory
1001     """
1002
1003     grub_dir = '/boot/grub/'
1004     mkdir(os.path.join(target, grub_dir))
1005
1006     f = open(target + grub_dir + 'loopback.cfg', 'w')
1007
1008     f.write("# grml2usb generated grub2 configuration file\n")
1009     f.write("source /boot/grub/header.cfg\n")
1010
1011     for defaults in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"):
1012         sourcefile = defaults.split(target + os.path.sep)[1]
1013         logging.debug("Found source file" + sourcefile)
1014         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1015
1016     for ops in glob.glob(target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"):
1017         sourcefile = ops.split(target + os.path.sep)[1]
1018         logging.debug("Found source file" + sourcefile)
1019         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1020
1021     f.write("source /boot/grub/addons.cfg\n")
1022     f.write("source /boot/grub/footer.cfg\n")
1023     f.close()
1024
1025
1026 def glob_and_copy(filepattern, dst):
1027     """Glob on specified filepattern and copy the result to dst
1028
1029     @filepattern: globbing pattern
1030     @dst: target directory
1031     """
1032     for name in glob.glob(filepattern):
1033         copy_if_exist(name, dst)
1034
1035
1036 def search_and_copy(filename, search_path, dst):
1037     """Search for the specified filename at searchpath and copy it to dst
1038
1039     @filename: filename to look for
1040     @search_path: base search file
1041     @dst: destionation to copy the file to
1042     """
1043     file_location = search_file(filename, search_path)
1044     copy_if_exist(file_location, dst)
1045
1046
1047 def copy_if_exist(filename, dst):
1048     """Copy filename to dst if filename is set.
1049
1050     @filename: a filename
1051     @dst: dst file
1052     """
1053     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1054         exec_rsync(filename, dst)
1055
1056
1057 def copy_bootloader_files(iso_mount, target, grml_flavour):
1058     """Copy grml's bootloader files to a given target
1059
1060     @iso_mount: path where a grml ISO is mounted on
1061     @target: path where grml's main files should be copied to
1062     @grml_flavour: name of the current processed grml_flavour
1063     """
1064
1065     syslinux_target = target + '/boot/syslinux/'
1066     execute(mkdir, syslinux_target)
1067
1068     grub_target = target + '/boot/grub/'
1069     execute(mkdir, grub_target)
1070
1071     logo = search_file('logo.16', iso_mount)
1072     exec_rsync(logo, syslinux_target + 'logo.16')
1073
1074     bootx64_efi = search_file('bootx64.efi', iso_mount)
1075     if bootx64_efi:
1076         mkdir(target + '/efi/boot/')
1077         exec_rsync(bootx64_efi, target + '/efi/boot/bootx64.efi')
1078
1079     efi_img = search_file('efi.img', iso_mount)
1080     if efi_img:
1081         mkdir(target + '/boot/')
1082         exec_rsync(efi_img, target + '/boot/efi.img')
1083
1084     for ffile in ['f%d' % number for number in range(1, 11)]:
1085         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1086
1087     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1088     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1089     if os.path.isfile(syslinux_target + 'ldlinux.sys'):
1090         os.unlink(syslinux_target + 'ldlinux.sys')
1091
1092     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1093     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1094
1095     if not source_dir:
1096         raise CriticalException(
1097             "file default.cfg could not be found.\n"
1098             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1099             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version.")
1100
1101     if not os.path.exists(iso_mount + '/boot/grub/footer.cfg'):
1102         logging.warning("Warning: Grml releases older than 2011.12 support only one flavour in grub.")
1103
1104     for expr in name, 'distri.cfg', \
1105         defaults_file, 'grml.png', 'hd.cfg', 'isolinux.cfg', 'isolinux.bin', \
1106         'isoprompt.cfg', 'options.cfg', \
1107         'prompt.cfg', 'vesamenu.cfg', 'grml.png', '*.c32':
1108         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1109
1110     for filename in glob.glob1(syslinux_target, "*.c32"):
1111         copy_if_exist(os.path.join(SYSLINUX_LIBS, filename), syslinux_target)
1112
1113     # copy the addons_*.cfg file to the new syslinux directory
1114     glob_and_copy(iso_mount + source_dir + 'addon*.cfg', syslinux_target)
1115
1116     search_and_copy('hidden.cfg', iso_mount + source_dir, syslinux_target + "new_" + 'hidden.cfg')
1117
1118     # copy all grub files from ISO
1119     glob_and_copy(iso_mount + '/boot/grub/*', grub_target)
1120
1121     # finally (after all GRUB files have been been installed) build static loopback.cfg
1122     build_loopbackcfg(target)
1123
1124
1125 def install_iso_files(grml_flavour, iso_mount, device, target):
1126     """Copy files from ISO to given target
1127
1128     @grml_flavour: name of grml flavour the configuration should be generated for
1129     @iso_mount: path where a grml ISO is mounted on
1130     @device: device/partition where bootloader should be installed to
1131     @target: path where grml's main files should be copied to"""
1132
1133     global GRML_DEFAULT
1134     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1135     if options.dryrun:
1136         return 0
1137     elif not options.bootloaderonly:
1138         logging.info("Copying files. This might take a while....")
1139         try:
1140             copy_system_files(grml_flavour, iso_mount, target)
1141             copy_grml_files(grml_flavour, iso_mount, target)
1142         except CriticalException as error:
1143             logging.critical("Execution failed: %s", error)
1144             sys.exit(1)
1145
1146     if not options.skipaddons:
1147         if not search_file('addons', iso_mount):
1148             logging.info("Could not find addons, therefore not installing.")
1149         else:
1150             copy_addons(iso_mount, target)
1151
1152     if not options.copyonly:
1153         copy_bootloader_files(iso_mount, target, grml_flavour)
1154
1155         if not options.dryrun:
1156             handle_bootloader_config(grml_flavour, device, target)
1157
1158     # make sure we sync filesystems before returning
1159     proc = subprocess.Popen(["sync"])
1160     proc.wait()
1161
1162
1163 def get_device_from_partition(partition):
1164     device = partition
1165     partition_number = None
1166     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1167         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1168         if m:
1169             device = m.group(1)
1170             partition_number = int(m.group(2)) - 1
1171     return (device, partition_number)
1172
1173
1174 def get_flavour(flavour_str):
1175     """Returns the flavour of a grml version string
1176     """
1177     return re.match(r'[\w-]*', flavour_str).group()
1178
1179
1180 def identify_grml_flavour(mountpath):
1181     """Get name of grml flavour
1182
1183     @mountpath: path where the grml ISO is mounted to
1184     @return: name of grml-flavour"""
1185
1186     version_files = search_file('grml-version', mountpath, lst_return=True)
1187
1188     if not version_files:
1189         if mountpath.startswith("/lib/live/mount/medium"):
1190             logging.critical("Error: could not find grml-version file.")
1191             logging.critical("Looks like your system is running from RAM but required files are not available.")
1192             logging.critical("Please either boot without toram=... or use boot option toram instead of toram=...")
1193         else:
1194             logging.critical("Error: could not find grml-version file.")
1195         cleanup()
1196         sys.exit(1)
1197
1198     flavours = []
1199     logging.debug("version_files = %s", version_files)
1200     for version_file in version_files:
1201         tmpfile = None
1202         try:
1203             tmpfile = open(version_file, 'r')
1204             for line in tmpfile.readlines():
1205                 flavours.append(get_flavour(line))
1206         finally:
1207             if tmpfile:
1208                 tmpfile.close()
1209
1210     return flavours
1211
1212
1213 def get_bootoptions(grml_flavour):
1214     """Returns bootoptions for specific flavour
1215
1216     @grml_flavour: name of the grml_flavour
1217     """
1218     # do NOT write "None" in kernel cmdline
1219     if not options.bootoptions:
1220         bootopt = ""
1221     else:
1222         bootopt = " ".join(options.bootoptions)
1223     bootopt = bootopt.replace("%flavour", grml_flavour)
1224     return bootopt
1225
1226
1227 def handle_grub_config(grml_flavour, device, target):
1228     """Main handler for generating grub (v1 and v2) configuration
1229
1230     @grml_flavour: name of grml flavour the configuration should be generated for
1231     @device: device/partition where grub should be installed to
1232     @target: path of grub's configuration files"""
1233
1234     global UUID
1235
1236     logging.debug("Updating grub configuration")
1237
1238     grub_target = target + '/boot/grub/'
1239
1240     bootid_re = re.compile("bootid=[\w_-]+")
1241     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1242
1243     bootopt = get_bootoptions(grml_flavour)
1244
1245     remove_regexes = []
1246     option_re = re.compile(r'(.*/boot/.*(linux26|vmlinuz).*)')
1247
1248     if options.removeoption:
1249         for regex in options.removeoption:
1250             remove_regexes.append(re.compile(regex))
1251
1252     shortname = get_shortname(grml_flavour)
1253     for filename in glob.glob(grub_target + '*.cfg'):
1254         for line in fileinput.input(filename, inplace=1):
1255             line = line.rstrip("\r\n")
1256             if option_re.search(line):
1257                 line = bootid_re.sub('', line)
1258                 if shortname in filename:
1259                     line = live_media_path_re.sub('', line)
1260                     line = line.rstrip() + ' live-media-path=/live/%s/ ' % (grml_flavour)
1261                 line = line.rstrip() + r' bootid=%s %s ' % (UUID, bootopt)
1262                 for regex in remove_regexes:
1263                     line = regex.sub(' ', line)
1264             print(line)
1265         fileinput.close()
1266
1267
1268 def initial_syslinux_config(target):
1269     """Generates intial syslinux configuration
1270
1271     @target path of syslinux's configuration files"""
1272
1273     target = target + "/"
1274     filename = target + "grmlmain.cfg"
1275     if os.path.isfile(target + "grmlmain.cfg"):
1276         return
1277     data = open(filename, "w")
1278     data.write(generate_main_syslinux_config())
1279     data.close()
1280
1281     filename = target + "hiddens.cfg"
1282     data = open(filename, "w")
1283     data.write("include hidden.cfg\n")
1284     data.close()
1285
1286
1287 def add_entry_if_not_present(filename, entry):
1288     """Write entry into filename if entry is not already in the file
1289
1290     @filanme: name of the file
1291     @entry: data to write to the file
1292     """
1293     data = open(filename, "a+")
1294     for line in data:
1295         if line == entry:
1296             break
1297     else:
1298         data.write(entry)
1299
1300     data.close()
1301
1302
1303 def get_flavour_filename(flavour):
1304     """Generate a iso9960 save filename out of the specified flavour
1305
1306     @flavour: grml flavour
1307     """
1308     return flavour.replace('-', '_')
1309
1310
1311 def adjust_syslinux_bootoptions(src, flavour):
1312     """Adjust existing bootoptions of specified syslinux config to
1313     grml2usb specific ones, e.g. change the location of the kernel...
1314
1315     @src: config file to alter
1316     @flavour: grml flavour
1317     """
1318
1319     append_re = re.compile("^(\s*append.*/boot/.*)$", re.I)
1320     # flavour_re = re.compile("(label.*)(grml\w+)")
1321     default_re = re.compile("(default.cfg)")
1322     bootid_re = re.compile("bootid=[\w_-]+")
1323     live_media_path_re = re.compile("live-media-path=[\w_/-]+")
1324
1325     bootopt = get_bootoptions(flavour)
1326
1327     regexe = []
1328     option_re = None
1329     if options.removeoption:
1330         option_re = re.compile(r'/boot/.*/(initrd.gz|initrd.img)')
1331
1332         for regex in options.removeoption:
1333             regexe.append(re.compile(r'%s' % regex))
1334
1335     for line in fileinput.input(src, inplace=1):
1336         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1337         line = default_re.sub(r'%s-\1' % flavour, line)
1338         line = bootid_re.sub('', line)
1339         line = live_media_path_re.sub('', line)
1340         line = append_re.sub(r'\1 live-media-path=/live/%s/ ' % flavour, line)
1341         line = append_re.sub(r'\1 boot=live %s ' % bootopt, line)
1342         line = append_re.sub(r'\1 %s=%s ' % ("bootid", UUID), line)
1343         if option_re and option_re.search(line):
1344             for regex in regexe:
1345                 line = regex.sub(' ', line)
1346         sys.stdout.write(line)
1347     fileinput.close()
1348
1349
1350 def adjust_labels(src, replacement):
1351     """Adjust the specified labels in the syslinux config file src with
1352     specified replacement
1353     """
1354     label_re = re.compile("^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1355     for line in fileinput.input(src, inplace=1):
1356         line = label_re.sub(replacement, line)
1357         sys.stdout.write(line)
1358     fileinput.close()
1359
1360
1361 def add_syslinux_entry(filename, grml_flavour):
1362     """Add includes for a specific grml_flavour to the specified filename
1363
1364     @filename: syslinux config file
1365     @grml_flavour: grml flavour to add
1366     """
1367
1368     entry_filename = "option_%s.cfg" % grml_flavour
1369     entry = "include %s\n" % entry_filename
1370
1371     add_entry_if_not_present(filename, entry)
1372     path = os.path.dirname(filename)
1373
1374     data = open(path + "/" + entry_filename, "w")
1375     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1376     data.close()
1377
1378
1379 def modify_filenames(grml_flavour, target, filenames):
1380     """Replace the standard filenames with the new ones
1381
1382     @grml_flavour: grml-flavour strin
1383     @target: directory where the files are located
1384     @filenames: list of filenames to alter
1385     """
1386     grml_filename = get_flavour_filename(grml_flavour)
1387     for filename in filenames:
1388         old_filename = "%s/%s" % (target, filename)
1389         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1390         os.rename(old_filename, new_filename)
1391
1392
1393 def remove_default_entry(filename):
1394     """Remove the default entry from specified syslinux file
1395
1396     @filename: syslinux config file
1397     """
1398     default_re = re.compile("^(\s*menu\s*default\s*)$", re.I)
1399     for line in fileinput.input(filename, inplace=1):
1400         if default_re.match(line):
1401             continue
1402         sys.stdout.write(line)
1403     fileinput.close()
1404
1405
1406 def handle_syslinux_config(grml_flavour, target):
1407     """Main handler for generating syslinux configuration
1408
1409     @grml_flavour: name of grml flavour the configuration should be generated for
1410     @target: path of syslinux's configuration files"""
1411
1412     logging.debug("Generating syslinux configuration")
1413     syslinux_target = target + '/boot/syslinux/'
1414     # should be present via  copy_bootloader_files(), but make sure it exits:
1415     execute(mkdir, syslinux_target)
1416     syslinux_cfg = syslinux_target + 'syslinux.cfg'
1417
1418     # install main configuration only *once*, no matter how many ISOs we have:
1419     syslinux_config_file = open(syslinux_cfg, 'w')
1420     syslinux_config_file.write("timeout 300\n")
1421     syslinux_config_file.write("include vesamenu.cfg\n")
1422     syslinux_config_file.close()
1423
1424     prompt_name = open(syslinux_target + 'promptname.cfg', 'w')
1425     prompt_name.write('menu label S^yslinux prompt\n')
1426     prompt_name.close()
1427
1428     initial_syslinux_config(syslinux_target)
1429     flavour_filename = get_flavour_filename(grml_flavour)
1430
1431     if search_file('default.cfg', syslinux_target):
1432         modify_filenames(grml_flavour, syslinux_target, ['grml.cfg', 'default.cfg'])
1433
1434     filename = search_file("new_hidden.cfg", syslinux_target)
1435
1436     # process hidden file
1437     if not search_file("hidden.cfg", syslinux_target):
1438         new_hidden = syslinux_target + "hidden.cfg"
1439         os.rename(filename, new_hidden)
1440         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1441     else:
1442         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1443         os.rename(filename, new_hidden_file)
1444         adjust_labels(new_hidden_file, r'\1 %s-\2' % grml_flavour)
1445         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1446         entry = 'include %s_hidden.cfg\n' % flavour_filename
1447         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1448
1449     new_default = "%s_default.cfg" % (flavour_filename)
1450     entry = 'include %s\n' % new_default
1451     defaults_file = '%s/defaults.cfg' % syslinux_target
1452     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1453     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1454
1455     if os.path.isfile(defaults_file):
1456
1457         # remove default menu entry in menu
1458         remove_default_entry(new_default_with_path)
1459
1460         # adjust all labels for additional isos
1461         adjust_labels(new_default_with_path, r'\1 %s' % grml_flavour)
1462         adjust_labels(new_grml_cfg, r'\1 %s-\2' % grml_flavour)
1463
1464     # always adjust bootoptions
1465     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1466     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1467
1468     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1469
1470     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1471
1472
1473 def handle_bootloader_config(grml_flavour, device, target):
1474     """Main handler for generating bootloader's configuration
1475
1476     @grml_flavour: name of grml flavour the configuration should be generated for
1477     @device: device/partition where bootloader should be installed to
1478     @target: path of bootloader's configuration files"""
1479
1480     global UUID
1481     UUID = get_uuid(target)
1482     if options.skipsyslinuxconfig:
1483         logging.info("Skipping generation of syslinux configuration as requested.")
1484     else:
1485         try:
1486             handle_syslinux_config(grml_flavour, target)
1487         except CriticalException as error:
1488             logging.critical("Fatal: %s", error)
1489             sys.exit(1)
1490
1491     if options.skipgrubconfig:
1492         logging.info("Skipping generation of grub configuration as requested.")
1493     else:
1494         try:
1495             handle_grub_config(grml_flavour, device, target)
1496         except CriticalException as error:
1497             logging.critical("Fatal: %s", error)
1498             sys.exit(1)
1499
1500
1501 def install(image, device):
1502     """Install a grml image to the specified device
1503
1504     @image: directory or is file
1505     @device: partition or directory to install the device
1506     """
1507     iso_mountpoint = image
1508     remove_image_mountpoint = False
1509     if os.path.isdir(image):
1510         if options.force or os.path.exists(os.path.join(image, 'live')):
1511             logging.info("Using %s as install base", image)
1512         else:
1513             q = raw_input("%s does not look like a Grml system. "
1514                 "Do you really want to use this image? y/N " % image)
1515             if q.lower() == 'y':
1516                 logging.info("Using %s as install base", image)
1517             else:
1518                 logging.info("Skipping install base %s", image)
1519     else:
1520         logging.info("Using ISO %s", image)
1521         iso_mountpoint = tempfile.mkdtemp(prefix="grml2usb", dir=os.path.abspath(options.tmpdir))
1522         register_tmpfile(iso_mountpoint)
1523         remove_image_mountpoint = True
1524         try:
1525             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1526         except CriticalException as error:
1527             logging.critical("Fatal: %s", error)
1528             sys.exit(1)
1529
1530     try:
1531         install_grml(iso_mountpoint, device)
1532     finally:
1533         if remove_image_mountpoint:
1534             try:
1535                 remove_mountpoint(iso_mountpoint)
1536             except CriticalException as error:
1537                 cleanup()
1538                 raise
1539
1540
1541 def install_grml(mountpoint, device):
1542     """Main logic for copying files of the currently running Grml system.
1543
1544     @mountpoint: directory where currently running live system resides (usually /lib/live/mount/medium)
1545     @device: partition where the specified ISO should be installed to"""
1546
1547     device_mountpoint = device
1548     if os.path.isdir(device):
1549         logging.info("Specified device is a directory, therefore not mounting.")
1550         remove_device_mountpoint = False
1551     else:
1552         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1553         register_tmpfile(device_mountpoint)
1554         remove_device_mountpoint = True
1555         try:
1556             check_for_fat(device)
1557             if not options.skipbootflag:
1558                 check_boot_flag(device)
1559             mount(device, device_mountpoint, ['-o', 'utf8,iocharset=iso8859-1'])
1560         except CriticalException as error:
1561             mount(device, device_mountpoint, "")
1562     try:
1563         grml_flavours = identify_grml_flavour(mountpoint)
1564         for flavour in set(grml_flavours):
1565             if not flavour:
1566                 logging.warning("No valid flavour found, please check your iso")
1567             logging.info("Identified grml flavour \"%s\".", flavour)
1568             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1569             GRML_FLAVOURS.add(flavour)
1570     finally:
1571         if remove_device_mountpoint:
1572             remove_mountpoint(device_mountpoint)
1573
1574
1575 def remove_mountpoint(mountpoint):
1576     """remove a registered mountpoint
1577     """
1578
1579     try:
1580         unmount(mountpoint, "")
1581         if os.path.isdir(mountpoint):
1582             os.rmdir(mountpoint)
1583             unregister_tmpfile(mountpoint)
1584     except CriticalException as error:
1585         cleanup()
1586         raise
1587
1588
1589 def handle_mbr(device):
1590     """Main handler for installing master boot record (MBR)
1591
1592     @device: device where the MBR should be installed to"""
1593
1594     if options.dryrun:
1595         logging.info("Would install MBR")
1596         return 0
1597
1598     mbr_device, partition_number = get_device_from_partition(device)
1599     if partition_number is None:
1600         logging.warn("Could not detect partition number, not activating partition")
1601
1602     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1603     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1604     if mbr_device == "/dev/loop":
1605         mbr_device = device
1606         logging.info("Detected loop device - using %s as MBR device therefore", mbr_device)
1607
1608     mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1609     if options.syslinuxmbr:
1610         mbrcode = ""
1611         mbr_locations = ('/usr/lib/syslinux/mbr.bin',
1612                          '/usr/lib/syslinux/bios/mbr.bin',
1613                          '/usr/share/syslinux/mbr.bin')
1614         for mbrpath in mbr_locations:
1615             if os.path.isfile(mbrpath):
1616                 mbrcode = mbrpath
1617                 break
1618
1619         if mbrcode is "":
1620             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1621             logging.error('Cannot find syslinux MBR, install it at %s)',
1622                           str_locations)
1623             raise CriticalException("syslinux MBR  can not be found at %s."
1624                                     % str_locations)
1625     elif options.mbrmenu:
1626         mbrcode = GRML2USB_BASE + '/mbr/mbrldr'
1627
1628     try:
1629         install_mbr(mbrcode, mbr_device, partition_number, True)
1630     except (IOError, Exception) as error:
1631         logging.critical("Execution failed: %s", error)
1632         sys.exit(1)
1633
1634
1635 def handle_vfat(device):
1636     """Check for FAT specific settings and options
1637
1638     @device: device that should checked / formated"""
1639
1640     # make sure we have mkfs.vfat available
1641     if options.fat16:
1642         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1643             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
1644             logging.critical('Please make sure to install dosfstools.')
1645             sys.exit(1)
1646
1647         if options.force:
1648             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1649         else:
1650             # make sure the user is aware of what he is doing
1651             f = raw_input("Are you sure you want to format the specified partition with fat16? y/N ")
1652             if f == "y" or f == "Y":
1653                 logging.info("Note: you can skip this question using the option --force")
1654             else:
1655                 sys.exit(1)
1656         try:
1657             mkfs_fat16(device)
1658         except CriticalException as error:
1659             logging.critical("Execution failed: %s", error)
1660             sys.exit(1)
1661
1662     # check for vfat filesystem
1663     if device is not None and not os.path.isdir(device) and options.syslinux:
1664         try:
1665             check_for_fat(device)
1666         except CriticalException as error:
1667             logging.critical("Execution failed: %s", error)
1668             sys.exit(1)
1669
1670     if options.skipusbcheck:
1671         logging.info("Not checking for removable USB device as requested via option --skip-usb-check.")
1672         return
1673
1674     if not os.path.isdir(device) and not check_for_usbdevice(device) and not options.force:
1675         print("Warning: the specified device %s does not look like a removable usb device." % device)
1676         f = raw_input("Do you really want to continue? y/N ")
1677         if f.lower() != "y":
1678             sys.exit(1)
1679
1680
1681 def handle_compat_warning(device):
1682     """Backwards compatible checks
1683
1684     @device: device that should be checked"""
1685
1686     # make sure we can replace old grml2usb script and warn user when using old way of life:
1687     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
1688         print("Warning: the semantics of grml2usb has changed.")
1689         print("Instead of using grml2usb /path/to/iso %s you might" % device)
1690         print("want to use grml2usb /path/to/iso /dev/... instead.")
1691         print("Please check out the grml2usb manpage for details.")
1692         f = raw_input("Do you really want to continue? y/N ")
1693         if f.lower() != "y":
1694             sys.exit(1)
1695
1696
1697 def handle_logging():
1698     """Log handling and configuration"""
1699
1700     if options.verbose and options.quiet:
1701         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
1702
1703     FORMAT = "%(message)s"
1704     if options.verbose:
1705         FORMAT = "%(asctime)-15s %(message)s"
1706         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
1707     elif options.quiet:
1708         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
1709     else:
1710         logging.basicConfig(level=logging.INFO, format=FORMAT)
1711
1712
1713 def handle_bootloader(device):
1714     """wrapper for installing bootloader
1715
1716     @device: device where bootloader should be installed to"""
1717
1718     # Install bootloader only if not using the --copy-only option
1719     if options.copyonly:
1720         logging.info("Not installing bootloader and its files as requested via option copyonly.")
1721     elif os.path.isdir(device):
1722         logging.info("Not installing bootloader as %s is a directory.", device)
1723     else:
1724         install_bootloader(device)
1725
1726
1727 def check_options(opts):
1728     """Check compability of provided user opts
1729
1730     @opts option dict from OptionParser
1731     """
1732     if opts.grubmbr and not opts.grub:
1733         raise CriticalException("--grub-mbr requires --grub option.")
1734
1735
1736 def check_programs():
1737     """check if all needed programs are installed"""
1738     if options.grub:
1739         global GRUB_INSTALL
1740         GRUB_INSTALL = which("grub-install") or which("grub2-install")
1741         if not GRUB_INSTALL:
1742             logging.critical("Fatal: grub-install not available (please install the "
1743                              + "grub package or drop the --grub option)")
1744             sys.exit(1)
1745
1746     if options.syslinux:
1747         if not which("syslinux"):
1748             logging.critical("Fatal: syslinux not available (please install the "
1749                              + "syslinux package or use the --grub option)")
1750             sys.exit(1)
1751
1752     if not which("rsync"):
1753         logging.critical("Fatal: rsync not available, can not continue - sorry.")
1754         sys.exit(1)
1755
1756
1757 def load_loop():
1758     """Runs modprobe loop and throws away it's output"""
1759     if not which("modprobe"):
1760         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
1761         logging.critical("Hint: is /sbin missing in PATH?")
1762         sys.exit(1)
1763
1764     proc = subprocess.Popen(["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1765     proc.wait()
1766
1767
1768 def main():
1769     """Main function [make pylint happy :)]"""
1770
1771     try:
1772         if options.version:
1773             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
1774             sys.exit(0)
1775
1776         if len(args) < 2:
1777             parser.error("invalid usage")
1778
1779         # log handling
1780         handle_logging()
1781
1782         # make sure we have the appropriate permissions
1783         check_uid_root()
1784
1785         check_options(options)
1786
1787         load_loop()
1788
1789         logging.info("Executing grml2usb version %s", PROG_VERSION)
1790
1791         if options.dryrun:
1792             logging.info("Running in simulation mode as requested via option dry-run.")
1793
1794         check_programs()
1795
1796         # specified arguments
1797         device = os.path.realpath(args[len(args) - 1])
1798         isos = args[0:len(args) - 1]
1799
1800         if not os.path.isdir(device):
1801             if device[-1:].isdigit():
1802                 if int(device[-1:]) > 4 or device[-2:].isdigit():
1803                     logging.critical("Fatal: installation on partition number >4 not supported. (BIOS won't support it.)")
1804                     sys.exit(1)
1805
1806         # provide upgrade path
1807         handle_compat_warning(device)
1808
1809         # check for vfat partition
1810         handle_vfat(device)
1811
1812         # main operation (like installing files)
1813         for iso in isos:
1814             install(iso, device)
1815
1816         # install mbr
1817         is_superfloppy = not device[-1:].isdigit()
1818         if is_superfloppy:
1819             logging.info("Detected superfloppy format - not installing MBR")
1820
1821         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
1822             handle_mbr(device)
1823
1824         handle_bootloader(device)
1825
1826         logging.info("Note: grml flavour %s was installed as the default booting system.", GRML_DEFAULT)
1827
1828         for flavour in GRML_FLAVOURS:
1829             logging.info("Note: you can boot flavour %s using '%s' on the commandline.", flavour, flavour)
1830
1831         # finally be polite :)
1832         logging.info("Finished execution of grml2usb (%s). Have fun with your Grml system.", PROG_VERSION)
1833
1834     except Exception as error:
1835         logging.critical("Fatal: %s", str(error))
1836         sys.exit(1)
1837
1838
1839 if __name__ == "__main__":
1840     try:
1841         main()
1842     except KeyboardInterrupt:
1843         logging.info("Received KeyboardInterrupt")
1844         cleanup()
1845
1846 ## END OF FILE #################################################################
1847 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8