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