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