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