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