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