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