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