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