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