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