Do not execute mkfs.fat without prompting; update docs
[grml2usb.git] / grml2usb.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 grml2usb
5 ~~~~~~~~
6
7 This script installs a grml system (either a running system or ISO[s]) to a USB device
8
9 :copyright: (c) 2009 by Michael Prokop <mika@grml.org>
10 :license: GPL v2 or any later version
11 :bugreports: http://grml.org/bugs/
12
13 TODO
14 ----
15
16 * install memtest, dos, grub,... to /boot/addons/
17 * copy grml files to /grml/
18 * implement missing options (--grub, --kernel, --initrd, --squashfs, --uninstall)
19 * code improvements:
20   - improve error handling wherever possible :)
21   - get rid of all TODOs in code
22   - use 'with open("...", "w") as f: ... f.write("...")'
23   - simplify functions/code as much as possible (move stuff to further functions) -> audit
24 * validate partition schema/layout: is the partition schema ok and the bootable flag set? (--validate?)
25 * implement logic for storing information about copied files -> register every file in a set()
26 * the last line in bootsplash (boot.msg) should mention all installed grml flavours
27 * graphical version? any volunteers? :)
28 """
29
30 from __future__ import with_statement
31 import os, re, subprocess, sys, tempfile
32 from optparse import OptionParser
33 from os.path import exists, join, abspath
34 from os import pathsep
35 from inspect import isroutine, isclass
36 import logging
37 import datetime, time
38
39 # global variables
40 PROG_VERSION = "0.0.1"
41 skip_mbr = True  # hm, can we get rid of that? :)
42 mounted = set()  # register mountpoints
43 tmpfiles = set() # register tmpfiles
44 datestamp= time.mktime(datetime.datetime.now().timetuple()) # unique identifier for syslinux.cfg
45
46 # cmdline parsing
47 usage = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/ice>\n\
48 \n\
49 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
50 Make sure you have at least a grml ISO or a running grml system (/live/image),\n\
51 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
52 and root access."
53
54 parser = OptionParser(usage=usage)
55 parser.add_option("--bootoptions", dest="bootoptions",
56                   action="store", type="string",
57                   help="use specified bootoptions as default")
58 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
59                   help="do not copy files but just install a bootloader")
60 parser.add_option("--copy-only", dest="copyonly", action="store_true",
61                   help="copy files only but do not install bootloader")
62 parser.add_option("--dry-run", dest="dryrun", action="store_true",
63                   help="avoid executing commands")
64 parser.add_option("--fat16", dest="fat16", action="store_true",
65                   help="format specified partition with FAT16")
66 parser.add_option("--force", dest="force", action="store_true",
67                   help="force any actions requiring manual interaction")
68 parser.add_option("--grub", dest="grub", action="store_true",
69                   help="install grub bootloader instead of syslinux [TODO]")
70 parser.add_option("--initrd", dest="initrd", action="store", type="string",
71                   help="install specified initrd instead of the default [TODO]")
72 parser.add_option("--kernel", dest="kernel", action="store", type="string",
73                   help="install specified kernel instead of the default [TODO]")
74 parser.add_option("--lilo", dest="lilo",  action="store", type="string",
75                   help="lilo executable to be used for installing MBR")
76 parser.add_option("--mbr", dest="mbr", action="store_true",
77                   help="install master boot record (MBR) on the device")
78 parser.add_option("--quiet", dest="quiet", action="store_true",
79                   help="do not output anything but just errors on console")
80 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
81                   help="install specified squashfs file instead of the default [TODO]")
82 parser.add_option("--uninstall", dest="uninstall", action="store_true",
83                   help="remove grml ISO files from specified device [TODO]")
84 parser.add_option("--verbose", dest="verbose", action="store_true",
85                   help="enable verbose mode")
86 parser.add_option("-v", "--version", dest="version", action="store_true",
87                   help="display version and exit")
88 (options, args) = parser.parse_args()
89
90
91 def cleanup():
92     """Cleanup function to make sure there aren't any mounted devices left behind.
93     """
94
95     logging.info("Cleaning up before exiting...")
96     proc = subprocess.Popen(["sync"])
97     proc.wait()
98
99     try:
100         for device in mounted:
101             unmount(device, "")
102     # ignore: RuntimeError: Set changed size during iteration
103     except:
104         pass
105
106
107 def get_function_name(obj):
108     if not (isroutine(obj) or isclass(obj)):
109         obj = type(obj)
110     return obj.__module__ + '.' + obj.__name__
111
112
113 def execute(f, *args):
114     """Wrapper for executing a command. Either really executes
115     the command (default) or when using --dry-run commandline option
116     just displays what would be executed."""
117     # usage: execute(subprocess.Popen, (["ls", "-la"]))
118     # TODO: doesn't work for proc = execute(subprocess.Popen...() -> any ideas?
119     if options.dryrun:
120         logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, args))))
121     else:
122         return f(*args)
123
124
125 def is_exe(fpath):
126     """Check whether a given file can be executed
127
128     @fpath: full path to file
129     @return:"""
130     return os.path.exists(fpath) and os.access(fpath, os.X_OK)
131
132
133 def which(program):
134     """Check whether a given program is available in PATH
135
136     @program: name of executable"""
137     fpath, fname = os.path.split(program)
138     if fpath:
139         if is_exe(program):
140             return program
141     else:
142         for path in os.environ["PATH"].split(os.pathsep):
143             exe_file = os.path.join(path, program)
144             if is_exe(exe_file):
145                 return exe_file
146
147     return None
148
149
150 def search_file(filename, search_path='/bin' + pathsep + '/usr/bin'):
151     """Given a search path, find file"""
152     file_found = 0
153     paths = search_path.split(pathsep)
154     for path in paths:
155         for current_dir, directories, files in os.walk(path):
156             if exists(join(current_dir, filename)):
157                 file_found = 1
158                 break
159     if file_found:
160         return abspath(join(current_dir, filename))
161     else:
162         return None
163
164
165 def check_uid_root():
166     """Check for root permissions"""
167     if not os.geteuid()==0:
168         sys.exit("Error: please run this script with uid 0 (root).")
169
170
171 def mkfs_fat16(device):
172     """Format specified device with VFAT/FAT16 filesystem.
173
174     @device: partition that should be formated"""
175
176     # syslinux -d boot/isolinux /dev/sdb1
177     logging.info("Formating partition with fat16 filesystem")
178     logging.debug("mkfs.vfat -F 16 %s" % device)
179     proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
180     proc.wait()
181     if proc.returncode != 0:
182         raise Exception, "error executing mkfs.vfat"
183
184
185 def install_syslinux(device):
186     """Install syslinux on specified device.
187
188     @device: partition where syslinux should be installed to"""
189
190     if options.dryrun:
191         logging.info("Would install syslinux as bootloader on %s", device)
192         return 0
193
194     # syslinux -d boot/isolinux /dev/sdb1
195     logging.info("Installing syslinux as bootloader")
196     logging.debug("syslinux -d boot/syslinux %s" % device)
197     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
198     proc.wait()
199     if proc.returncode != 0:
200         raise Exception, "error executing syslinux"
201
202
203 def generate_grub_config(grml_flavour):
204     """Generate grub configuration for use via menu.lst
205
206     @grml_flavour: name of grml flavour the configuration should be generated for"""
207     # TODO
208     # * what about systems using grub2 without having grub1 available?
209     # * support grub2?
210
211     return("""\
212 # misc options:
213 timeout 30
214 # color red/blue green/black
215 splashimage=/boot/grub/splash.xpm.gz
216 foreground  = 000000
217 background  = FFCC33
218
219 # define entries:
220 title %(grml_flavour)s  - Default boot (using 1024x768 framebuffer)
221 kernel /boot/release/%(grml_flavour)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_flavour)s
222 initrd /boot/release/%(grml_flavour)s/initrd.gz
223
224 # TODO: extend configuration :)
225 """ % locals())
226
227
228 def generate_isolinux_splash(grml_flavour):
229     """Generate bootsplash for isolinux/syslinux
230
231     @grml_flavour: name of grml flavour the configuration should be generated for"""
232
233     # TODO: adjust last bootsplash line (the one following the "Some information and boot ...")
234
235     grml_name = grml_flavour
236
237     return("""\
238 \ f17\f\18/boot/syslinux/logo.16
239
240 Some information and boot options available via keys F2 - F10. http://grml.org/
241 %(grml_name)s
242 """ % locals())
243
244
245 def generate_main_syslinux_config(grml_flavour, bootoptions):
246     """Generate main configuration for use in syslinux.cfg
247
248     @grml_flavour: name of grml flavour the configuration should be generated for
249     @bootoptions: bootoptions that should be used as a default"""
250
251     local_datestamp = datestamp
252
253     return("""\
254 ## main syslinux configuration - generated by grml2usb [main config generated at: %(local_datestamp)s]
255 # use this to control the bootup via a serial port
256 # SERIAL 0 9600
257 DEFAULT grml
258 TIMEOUT 300
259 PROMPT 1
260 DISPLAY /boot/syslinux/boot.msg
261 F1 /boot/syslinux/boot.msg
262 F2 /boot/syslinux/f2
263 F3 /boot/syslinux/f3
264 F4 /boot/syslinux/f4
265 F5 /boot/syslinux/f5
266 F6 /boot/syslinux/f6
267 F7 /boot/syslinux/f7
268 F8 /boot/syslinux/f8
269 F9 /boot/syslinux/f9
270 F10 /boot/syslinux/f10
271 ## end of main configuration
272
273 ## global configuration
274 # the default option (using %(grml_flavour)s)
275 LABEL  grml
276 KERNEL /boot/release/%(grml_flavour)s/linux26
277 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(bootoptions)s
278
279 # memtest
280 LABEL  memtest
281 KERNEL /boot/addons/memtest
282 APPEND BOOT_IMAGE=memtest
283
284 # grub
285 LABEL grub
286 MENU LABEL grub
287 KERNEL /boot/addons/memdisk
288 APPEND initrd=/boot/addons/allinone.img
289
290 # dos
291 LABEL dos
292 MENU LABEL dos
293 KERNEL /boot/addons/memdisk
294 APPEND initrd=/boot/addons/balder10.imz
295
296 ## end of global configuration
297 """ % locals())
298
299
300 def generate_flavour_specific_syslinux_config(grml_flavour, bootoptions):
301     """Generate flavour specific configuration for use in syslinux.cfg
302
303     @grml_flavour: name of grml flavour the configuration should be generated for
304     @bootoptions: bootoptions that should be used as a default"""
305
306     local_datestamp = datestamp
307
308     return("""\
309
310 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
311 LABEL  %(grml_flavour)s
312 KERNEL /boot/release/%(grml_flavour)s/linux26
313 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(bootoptions)s
314
315 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
316 LABEL  %(grml_flavour)s2ram
317 KERNEL /boot/release/%(grml_flavour)s/linux26
318 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s toram=%(grml_flavour)s %(bootoptions)s
319
320 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
321 LABEL  %(grml_flavour)s-debug
322 KERNEL /boot/release/%(grml_flavour)s/linux26
323 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s debug boot=live initcall_debug%(bootoptions)s
324
325 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
326 LABEL  %(grml_flavour)s-x
327 KERNEL /boot/release/%(grml_flavour)s/linux26
328 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s startx=wm-ng %(bootoptions)s
329
330 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
331 LABEL  %(grml_flavour)s-nofb
332 KERNEL /boot/release/%(grml_flavour)s/linux26
333 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal video=ofonly %(bootoptions)s
334
335 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
336 LABEL  %(grml_flavour)s-failsafe
337 KERNEL /boot/release/%(grml_flavour)s/linux26
338 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal lang=us boot=live noautoconfig atapicd noacpi acpi=off nomodules nofirewire noudev nousb nohotplug noapm nopcmcia maxcpus=1 noscsi noagp nodma ide=nodma noswap nofstab nosound nogpm nosyslog nodhcp nocpu nodisc nomodem xmodule=vesa noraid nolvm %(bootoptions)s
339
340 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
341 LABEL  %(grml_flavour)s-forensic
342 KERNEL /boot/release/%(grml_flavour)s/linux26
343 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s nofstab noraid nolvm noautoconfig noswap raid=noautodetect %(bootoptions)s
344
345 # flavour specific configuration for %(grml_flavour)s [grml2usb for %(grml_flavour)s: %(local_datestamp)s]
346 LABEL  %(grml_flavour)s-serial
347 KERNEL /boot/release/%(grml_flavour)s/linux26
348 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s vga=normal video=vesafb:off  console=tty1 console=ttyS0,9600n8 %(bootoptions)s
349 """ % locals())
350
351
352 def install_grub(device):
353     """Install grub on specified device.
354
355     @device: partition where grub should be installed to"""
356
357     if options.dryrun:
358         logging.info("Would execute grub-install %s now.", device)
359     else:
360         logging.critical("TODO: sorry - grub-install %s not implemented yet"  % device)
361
362
363 def install_bootloader(device):
364     """Install bootloader on specified device.
365
366     @device: partition where bootloader should be installed to"""
367
368     # Install bootloader on the device (/dev/sda),
369     # not on the partition itself (/dev/sda1)?
370     #if partition[-1:].isdigit():
371     #    device = re.match(r'(.*?)\d*$', partition).group(1)
372     #else:
373     #    device = partition
374
375     if options.grub:
376         install_grub(device)
377     else:
378         install_syslinux(device)
379
380
381 def is_writeable(device):
382     """Check if the device is writeable for the current user
383
384     @device: partition where bootloader should be installed to"""
385
386     if not device:
387         return False
388         #raise Exception, "no device for checking write permissions"
389
390     if not os.path.exists(device):
391         return False
392
393     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
394
395
396 def install_mbr(device):
397     """Install a default master boot record on given device
398
399     @device: device where MBR should be installed to"""
400
401     if not is_writeable(device):
402         raise IOError, "device not writeable for user"
403
404     if options.lilo:
405         lilo = options.lilo
406     else:
407         lilo = '/usr/share/grml2usb/lilo/lilo.static'
408
409     if not is_exe(lilo):
410         raise Exception, "lilo executable can not be execute"
411
412     if options.dryrun:
413         logging.info("Would install MBR running lilo and using syslinux.")
414         return 0
415
416     # to support -A for extended partitions:
417     logging.info("Installing MBR")
418     logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
419     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
420     proc.wait()
421     if proc.returncode != 0:
422         raise Exception, "error executing lilo"
423
424     # activate partition:
425     logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
426     if not options.dryrun:
427         proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
428         proc.wait()
429         if proc.returncode != 0:
430             raise Exception, "error executing lilo"
431
432     # lilo's mbr is broken, use the one from syslinux instead:
433     if not os.path.isfile("/usr/lib/syslinux/mbr.bin"):
434         raise Exception, "/usr/lib/syslinux/mbr.bin can not be read"
435
436     logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
437     if not options.dryrun:
438         try:
439             # TODO -> use Popen instead?
440             retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
441             if retcode < 0:
442                 logging.critical("Error copying MBR to device (%s)" % retcode)
443         except OSError, error:
444             logging.critical("Execution failed:", error)
445
446
447 def register_tmpfile(path):
448     """TODO
449     """
450
451     tmpfiles.add(path)
452
453
454 def unregister_tmpfile(path):
455     """TODO
456     """
457
458     if path in tmpfiles:
459         tmpfiles.remove(path)
460
461
462 def register_mountpoint(target):
463     """TODO
464     """
465
466     mounted.add(target)
467
468
469 def unregister_mountpoint(target):
470     """TODO
471     """
472
473     if target in mounted:
474         mounted.remove(target)
475
476
477 def mount(source, target, options):
478     """Mount specified source on given target
479
480     @source: name of device/ISO that should be mounted
481     @target: directory where the ISO should be mounted to
482     @options: mount specific options"""
483
484     # notice: options.dryrun does not work here, as we have to
485     # locate files and identify the grml flavour
486     logging.debug("mount %s %s %s" % (options, source, target))
487     proc = subprocess.Popen(["mount"] + list(options) + [source, target])
488     proc.wait()
489     if proc.returncode != 0:
490         raise Exception, "Error executing mount"
491     else:
492         logging.debug("register_mountpoint(%s)" % target)
493         register_mountpoint(target)
494
495
496 def unmount(target, options):
497     """Unmount specified target
498
499     @target: target where something is mounted on and which should be unmounted
500     @options: options for umount command"""
501
502     # make sure we unmount only already mounted targets
503     target_unmount = False
504     mounts = open('/proc/mounts').readlines()
505     mountstring = re.compile(".*%s.*" % re.escape(target))
506     for line in mounts:
507         if re.match(mountstring, line):
508             target_unmount = True
509
510     if not target_unmount:
511         logging.debug("%s not mounted anymore" % target)
512     else:
513         logging.debug("umount %s %s" % (list(options), target))
514         proc = subprocess.Popen(["umount"] + list(options) + [target])
515         proc.wait()
516         if proc.returncode != 0:
517             raise Exception, "Error executing umount"
518         else:
519             logging.debug("unregister_mountpoint(%s)" % target)
520             unregister_mountpoint(target)
521
522
523 def check_for_usbdevice(device):
524     """Check whether the specified device is a removable USB device
525
526     @device: device name, like /dev/sda1 or /dev/sda
527     """
528
529     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
530     usbdevice = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
531     if os.path.isfile(usbdevice):
532         is_usb = open(usbdevice).readline()
533         if is_usb == "1":
534             return 0
535         else:
536             return 1
537
538
539 def check_for_fat(partition):
540     """Check whether specified partition is a valid VFAT/FAT16 filesystem
541
542     @partition: device name of partition"""
543
544     try:
545         udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t", partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
546         filesystem = udev_info.communicate()[0].rstrip()
547
548         if udev_info.returncode == 2:
549             raise Exception, "Failed to read device %s - wrong UID / permissions?" % partition
550
551         if filesystem != "vfat":
552             raise Exception, "Device %s does not contain a FAT16 partition." % partition
553
554     except OSError:
555         raise Exception, "Sorry, /lib/udev/vol_id not available."
556
557
558 def mkdir(directory):
559     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
560
561     if not os.path.isdir(directory):
562         try:
563             os.makedirs(directory)
564         except OSError:
565             # just silently pass as it's just fine it the directory exists
566             pass
567
568
569 def copy_grml_files(grml_flavour, iso_mount, target):
570     """Copy files from ISO on given target"""
571
572     # TODO
573     # * provide alternative search_file() if file information is stored in a config.ini file?
574     # * catch "install: .. No space left on device" & CO
575     # * abstract copy logic to make the code shorter and get rid of spaghettis ;)
576
577     if options.dryrun:
578         logging.info("Would copy files to %s", iso_mount)
579     elif not options.bootloaderonly:
580         logging.info("Copying files. This might take a while....")
581
582         squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
583         squashfs_target = target + '/live/'
584         execute(mkdir, squashfs_target)
585
586         # use install(1) for now to make sure we can write the files afterwards as normal user as well
587         logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
588         proc = subprocess.Popen(["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
589         proc.wait()
590
591         filesystem_module = search_file('filesystem.module', iso_mount)
592         logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
593         proc = subprocess.Popen(["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
594         proc.wait()
595
596         release_target = target + '/boot/release/' + grml_flavour
597         execute(mkdir, release_target)
598
599         kernel = search_file('linux26', iso_mount)
600         logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
601         proc = subprocess.Popen(["install", "--mode=664", kernel, release_target + '/linux26'])
602         proc.wait()
603
604         initrd = search_file('initrd.gz', iso_mount)
605         logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
606         proc = subprocess.Popen(["install", "--mode=664", initrd, release_target + '/initrd.gz'])
607         proc.wait()
608
609     if not options.copyonly:
610         syslinux_target = target + '/boot/syslinux/'
611         execute(mkdir, syslinux_target)
612
613         logo = search_file('logo.16', iso_mount)
614         logging.debug("cp %s %s" % (logo, syslinux_target + 'logo.16'))
615         proc = subprocess.Popen(["install", "--mode=664", logo, syslinux_target + 'logo.16'])
616         proc.wait()
617
618         for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
619             bootsplash = search_file(ffile, iso_mount)
620             logging.debug("cp %s %s" % (bootsplash, syslinux_target + ffile))
621             proc = subprocess.Popen(["install", "--mode=664", bootsplash, syslinux_target + ffile])
622             proc.wait()
623
624         grub_target = target + '/boot/grub/'
625         execute(mkdir, grub_target)
626
627         if not os.path.isfile("/usr/share/grml2usb/grub/splash.xpm.gz"):
628             logging.critical("Error: /usr/share/grml2usb/grub/splash.xpm.gz can not be read.")
629             raise
630         else:
631             logging.debug("cp /usr/share/grml2usb/grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz')
632             proc = subprocess.Popen(["install", "--mode=664", '/usr/share/grml2usb/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz'])
633             proc.wait()
634
635         if not os.path.isfile("/usr/share/grml2usb/grub/stage2_eltorito"):
636             logging.critical("Error: /usr/share/grml2usb/grub/stage2_eltorito can not be read.")
637             raise
638         else:
639             logging.debug("cp /usr/share/grml2usb/grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito')
640             proc = subprocess.Popen(["install", "--mode=664", '/usr/share/grml2usb/grub/stage2_eltorito', grub_target + 'stage2_eltorito'])
641             proc.wait()
642
643         if not options.dryrun:
644             logging.debug("Generating grub configuration")
645             #with open("...", "w") as f:
646             #f.write("bla bla bal")
647             grub_config_file = open(grub_target + 'menu.lst', 'w')
648             grub_config_file.write(generate_grub_config(grml_flavour))
649             grub_config_file.close()
650
651             logging.info("Generating syslinux configuration")
652             syslinux_cfg = syslinux_target + 'syslinux.cfg'
653
654             # install main configuration only *once*, no matter how many ISOs we have:
655             if os.path.isfile(syslinux_cfg):
656                 string = open(syslinux_cfg).readline()
657                 main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(datestamp)))
658                 if not re.match(main_identifier, string):
659                     syslinux_config_file = open(syslinux_cfg, 'w')
660                     logging.info("Notice: grml flavour %s is being installed as the default booting system." % grml_flavour)
661                     syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
662                     syslinux_config_file.close()
663             else:
664                     syslinux_config_file = open(syslinux_cfg, 'w')
665                     syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
666                     syslinux_config_file.close()
667
668             # install flavour specific configuration only *once* as well
669             # kind of ugly - I'm pretty sure this could be smoother...
670             flavour_config = True
671             if os.path.isfile(syslinux_cfg):
672                 string = open(syslinux_cfg).readlines()
673                 logging.info("Notice: you can boot flavour %s using '%s' on the commandline." % (grml_flavour, grml_flavour))
674                 flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(datestamp))))
675                 for line in string:
676                     if flavour.match(line):
677                         flavour_config = False
678
679
680             if flavour_config:
681                 syslinux_config_file = open(syslinux_cfg, 'a')
682                 syslinux_config_file.write(generate_flavour_specific_syslinux_config(grml_flavour, options.bootoptions))
683                 syslinux_config_file.close( )
684
685             logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
686             isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
687             isolinux_splash.write(generate_isolinux_splash(grml_flavour))
688             isolinux_splash.close( )
689
690
691     # make sure we sync filesystems before returning
692     proc = subprocess.Popen(["sync"])
693     proc.wait()
694
695
696 def uninstall_files(device):
697     """Get rid of all grml files on specified device
698
699     @device: partition where grml2usb files should be removed from"""
700
701     # TODO
702     logging.critical("TODO: uninstalling files from %s not yet implement, sorry." % device)
703
704
705 def identify_grml_flavour(mountpath):
706     """Get name of grml flavour
707
708     @mountpath: path where the grml ISO is mounted to
709     @return: name of grml-flavour"""
710
711     version_file = search_file('grml-version', mountpath)
712
713     if version_file == "":
714         logging.critical("Error: could not find grml-version file.")
715         raise
716
717     try:
718         tmpfile = open(version_file, 'r')
719         grml_info = tmpfile.readline()
720         grml_flavour = re.match(r'[\w-]*', grml_info).group()
721     except TypeError:
722         raise
723     except:
724         logging.critical("Unexpected error:", sys.exc_info()[0])
725         raise
726
727     return grml_flavour
728
729
730 def handle_iso(iso, device):
731     """Main logic for mounting ISOs and copying files.
732
733     @iso: full path to the ISO that should be installed to the specified device
734     @device: partition where the specified ISO should be installed to"""
735
736     logging.info("Using ISO %s" % iso)
737
738     if os.path.isdir(iso):
739         logging.critical("TODO: /live/image handling not yet implemented - sorry") # TODO
740         sys.exit(1)
741     else:
742         iso_mountpoint = tempfile.mkdtemp()
743         register_tmpfile(iso_mountpoint)
744         remove_iso_mountpoint = True
745
746         if not os.path.isfile(iso):
747             logging.critical("Fatal: specified ISO %s could not be read" % iso)
748             cleanup()
749             sys.exit(1)
750
751         mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
752
753         if os.path.isdir(device):
754             logging.info("Specified target is a directory, not mounting therefore.")
755             device_mountpoint = device
756             remove_device_mountpoint = False
757             skip_mbr = True
758
759         else:
760             device_mountpoint = tempfile.mkdtemp()
761             register_tmpfile(device_mountpoint)
762             remove_device_mountpoint = True
763             try:
764                 mount(device, device_mountpoint, "")
765             except Exception, error:
766                 logging.critical("Fatal: %s" % error)
767                 cleanup()
768
769         try:
770             grml_flavour = identify_grml_flavour(iso_mountpoint)
771             logging.info("Identified grml flavour \"%s\"." % grml_flavour)
772             copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint)
773         except TypeError:
774             logging.critical("Fatal: a critical error happend during execution, giving up")
775             sys.exit(1)
776         finally:
777             if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
778                 unmount(iso_mountpoint, "")
779
780                 os.rmdir(iso_mountpoint)
781                 unregister_tmpfile(iso_mountpoint)
782
783             if remove_device_mountpoint:
784                 unmount(device_mountpoint, "")
785
786                 if os.path.isdir(device_mountpoint):
787                     os.rmdir(device_mountpoint)
788                     unregister_tmpfile(device_mountpoint)
789
790         # grml_flavour_short = grml_flavour.replace('-','')
791         # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
792
793
794 def main():
795     """Main function [make pylint happy :)]"""
796
797     if options.version:
798         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
799         sys.exit(0)
800
801     if len(args) < 2:
802         parser.error("invalid usage")
803
804     # log handling
805     if options.verbose:
806         FORMAT = "%(asctime)-15s %(message)s"
807         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
808     elif options.quiet:
809         FORMAT = "Critial: %(message)s"
810         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
811     else:
812         FORMAT = "Info: %(message)s"
813         logging.basicConfig(level=logging.INFO, format=FORMAT)
814
815     if options.dryrun:
816         logging.info("Running in simulate mode as requested via option dry-run.")
817     else:
818         check_uid_root()
819
820     # specified arguments
821     device = args[len(args) - 1]
822     isos = args[0:len(args) - 1]
823
824     # make sure we can replace old grml2usb script and warn user when using old way of life:
825     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
826         print "Warning: the semantics of grml2usb has changed."
827         print "Instead of using grml2usb /path/to/iso %s you might" % device
828         print "want to use grml2usb /path/to/iso /dev/... instead."
829         print "Please check out the grml2usb manpage for details."
830         f = raw_input("Do you really want to continue? y/N ")
831         if f == "y" or f == "Y":
832            pass
833         else:
834             sys.exit(1)
835
836     # make sure we have syslinux available
837     if options.mbr:
838         if not which("syslinux") and not options.copyonly and not options.dryrun:
839             logging.critical('Sorry, syslinux not available. Exiting.')
840             logging.critical('Please install syslinux or consider using the --grub option.')
841             sys.exit(1)
842
843     # make sure we have mkfs.vfat available
844     if options.fat16 and not options.force:
845         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
846             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
847             logging.critical('Please make sure to install dosfstools.')
848             sys.exit(1)
849
850         # make sure the user is aware of what he is doing
851         f = raw_input("Are you sure you want to format the device with a fat16 filesystem? y/N ")
852         if f == "y" or f == "Y":
853             logging.info("Note: you can skip this question using the option --force")
854             mkfs_fat16(device)
855         else:
856             sys.exit(1)
857
858     # check for vfat filesystem
859     if device is not None and not os.path.isdir(device):
860         try:
861             check_for_fat(device)
862         except Exception, error:
863             logging.critical("Execution failed: %s", error)
864             sys.exit(1)
865
866     if not check_for_usbdevice(device):
867         print "Warning: the specified device %s does not look like a removable usb device." % device
868         f = raw_input("Do you really want to continue? y/N ")
869         if f == "y" or f == "Y":
870            pass
871         else:
872             sys.exit(1)
873
874     # format partition:
875     if options.fat16:
876         mkfs_fat16(device)
877
878     # main operation (like installing files)
879     for iso in isos:
880         handle_iso(iso, device)
881
882     # install MBR
883     if not options.mbr or skip_mbr:
884         logging.info("You are NOT using the --mbr option. Consider using it if your device does not boot.")
885     else:
886         # make sure we install MBR on /dev/sdX and not /dev/sdX#
887         if device[-1:].isdigit():
888             mbr_device = re.match(r'(.*?)\d*$', device).group(1)
889
890         try:
891             install_mbr(mbr_device)
892         except IOError, error:
893             logging.critical("Execution failed: %s", error)
894             sys.exit(1)
895         except Exception, error:
896             logging.critical("Execution failed: %s", error)
897             sys.exit(1)
898
899     # Install bootloader only if not using the --copy-only option
900     if options.copyonly:
901         logging.info("Not installing bootloader and its files as requested via option copyonly.")
902     else:
903         install_bootloader(device)
904
905     # finally be politely :)
906     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
907
908
909 if __name__ == "__main__":
910     try:
911         main()
912     except KeyboardInterrupt:
913         logging.info("Received KeyboardInterrupt")
914         cleanup()
915
916 ## END OF FILE #################################################################
917 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8