30abbc65b7759d6ed615c8d154852235207f15bc
[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.9.2"
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         from platform import architecture
408         if architecture()[0] == '64bit':
409             lilo = '/usr/share/grml2usb/lilo/lilo.static.amd64'
410         else:
411             lilo = '/usr/share/grml2usb/lilo/lilo.static.i386'
412
413     if not is_exe(lilo):
414         raise Exception, "lilo executable can not be execute"
415
416     if options.dryrun:
417         logging.info("Would install MBR running lilo and using syslinux.")
418         return 0
419
420     # to support -A for extended partitions:
421     logging.info("Installing MBR")
422     logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
423     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
424     proc.wait()
425     if proc.returncode != 0:
426         raise Exception, "error executing lilo"
427
428     # activate partition:
429     logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
430     if not options.dryrun:
431         proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
432         proc.wait()
433         if proc.returncode != 0:
434             raise Exception, "error executing lilo"
435
436     # lilo's mbr is broken, use the one from syslinux instead:
437     if not os.path.isfile("/usr/lib/syslinux/mbr.bin"):
438         raise Exception, "/usr/lib/syslinux/mbr.bin can not be read"
439
440     logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
441     if not options.dryrun:
442         try:
443             # TODO -> use Popen instead?
444             retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
445             if retcode < 0:
446                 logging.critical("Error copying MBR to device (%s)" % retcode)
447         except OSError, error:
448             logging.critical("Execution failed:", error)
449
450
451 def register_tmpfile(path):
452     """TODO
453     """
454
455     tmpfiles.add(path)
456
457
458 def unregister_tmpfile(path):
459     """TODO
460     """
461
462     if path in tmpfiles:
463         tmpfiles.remove(path)
464
465
466 def register_mountpoint(target):
467     """TODO
468     """
469
470     mounted.add(target)
471
472
473 def unregister_mountpoint(target):
474     """TODO
475     """
476
477     if target in mounted:
478         mounted.remove(target)
479
480
481 def mount(source, target, options):
482     """Mount specified source on given target
483
484     @source: name of device/ISO that should be mounted
485     @target: directory where the ISO should be mounted to
486     @options: mount specific options"""
487
488     # notice: options.dryrun does not work here, as we have to
489     # locate files and identify the grml flavour
490     logging.debug("mount %s %s %s" % (options, source, target))
491     proc = subprocess.Popen(["mount"] + list(options) + [source, target])
492     proc.wait()
493     if proc.returncode != 0:
494         raise Exception, "Error executing mount"
495     else:
496         logging.debug("register_mountpoint(%s)" % target)
497         register_mountpoint(target)
498
499
500 def unmount(target, options):
501     """Unmount specified target
502
503     @target: target where something is mounted on and which should be unmounted
504     @options: options for umount command"""
505
506     # make sure we unmount only already mounted targets
507     target_unmount = False
508     mounts = open('/proc/mounts').readlines()
509     mountstring = re.compile(".*%s.*" % re.escape(target))
510     for line in mounts:
511         if re.match(mountstring, line):
512             target_unmount = True
513
514     if not target_unmount:
515         logging.debug("%s not mounted anymore" % target)
516     else:
517         logging.debug("umount %s %s" % (list(options), target))
518         proc = subprocess.Popen(["umount"] + list(options) + [target])
519         proc.wait()
520         if proc.returncode != 0:
521             raise Exception, "Error executing umount"
522         else:
523             logging.debug("unregister_mountpoint(%s)" % target)
524             unregister_mountpoint(target)
525
526
527 def check_for_usbdevice(device):
528     """Check whether the specified device is a removable USB device
529
530     @device: device name, like /dev/sda1 or /dev/sda
531     """
532
533     usbdevice = re.match(r'/dev/(.*?)\d*$', device).group(1)
534     usbdevice = os.path.realpath('/sys/class/block/' + usbdevice + '/removable')
535     if os.path.isfile(usbdevice):
536         is_usb = open(usbdevice).readline()
537         if is_usb == "1":
538             return 0
539         else:
540             return 1
541
542
543 def check_for_fat(partition):
544     """Check whether specified partition is a valid VFAT/FAT16 filesystem
545
546     @partition: device name of partition"""
547
548     try:
549         udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t", partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
550         filesystem = udev_info.communicate()[0].rstrip()
551
552         if udev_info.returncode == 2:
553             raise Exception, "Failed to read device %s - wrong UID / permissions?" % partition
554
555         if filesystem != "vfat":
556             raise Exception, "Device %s does not contain a FAT16 partition." % partition
557
558     except OSError:
559         raise Exception, "Sorry, /lib/udev/vol_id not available."
560
561
562 def mkdir(directory):
563     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
564
565     if not os.path.isdir(directory):
566         try:
567             os.makedirs(directory)
568         except OSError:
569             # just silently pass as it's just fine it the directory exists
570             pass
571
572
573 def copy_grml_files(grml_flavour, iso_mount, target):
574     """Copy files from ISO on given target"""
575
576     # TODO
577     # * provide alternative search_file() if file information is stored in a config.ini file?
578     # * catch "install: .. No space left on device" & CO
579     # * abstract copy logic to make the code shorter and get rid of spaghettis ;)
580
581     if options.dryrun:
582         logging.info("Would copy files to %s", iso_mount)
583     elif not options.bootloaderonly:
584         logging.info("Copying files. This might take a while....")
585
586         squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
587         squashfs_target = target + '/live/'
588         execute(mkdir, squashfs_target)
589
590         # use install(1) for now to make sure we can write the files afterwards as normal user as well
591         logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
592         proc = subprocess.Popen(["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
593         proc.wait()
594
595         filesystem_module = search_file('filesystem.module', iso_mount)
596         logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
597         proc = subprocess.Popen(["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
598         proc.wait()
599
600         release_target = target + '/boot/release/' + grml_flavour
601         execute(mkdir, release_target)
602
603         kernel = search_file('linux26', iso_mount)
604         logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
605         proc = subprocess.Popen(["install", "--mode=664", kernel, release_target + '/linux26'])
606         proc.wait()
607
608         initrd = search_file('initrd.gz', iso_mount)
609         logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
610         proc = subprocess.Popen(["install", "--mode=664", initrd, release_target + '/initrd.gz'])
611         proc.wait()
612
613     if not options.copyonly:
614         syslinux_target = target + '/boot/syslinux/'
615         execute(mkdir, syslinux_target)
616
617         logo = search_file('logo.16', iso_mount)
618         logging.debug("cp %s %s" % (logo, syslinux_target + 'logo.16'))
619         proc = subprocess.Popen(["install", "--mode=664", logo, syslinux_target + 'logo.16'])
620         proc.wait()
621
622         for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
623             bootsplash = search_file(ffile, iso_mount)
624             logging.debug("cp %s %s" % (bootsplash, syslinux_target + ffile))
625             proc = subprocess.Popen(["install", "--mode=664", bootsplash, syslinux_target + ffile])
626             proc.wait()
627
628         grub_target = target + '/boot/grub/'
629         execute(mkdir, grub_target)
630
631         if not os.path.isfile("/usr/share/grml2usb/grub/splash.xpm.gz"):
632             logging.critical("Error: /usr/share/grml2usb/grub/splash.xpm.gz can not be read.")
633             raise
634         else:
635             logging.debug("cp /usr/share/grml2usb/grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz')
636             proc = subprocess.Popen(["install", "--mode=664", '/usr/share/grml2usb/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz'])
637             proc.wait()
638
639         if not os.path.isfile("/usr/share/grml2usb/grub/stage2_eltorito"):
640             logging.critical("Error: /usr/share/grml2usb/grub/stage2_eltorito can not be read.")
641             raise
642         else:
643             logging.debug("cp /usr/share/grml2usb/grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito')
644             proc = subprocess.Popen(["install", "--mode=664", '/usr/share/grml2usb/grub/stage2_eltorito', grub_target + 'stage2_eltorito'])
645             proc.wait()
646
647         if not options.dryrun:
648             logging.debug("Generating grub configuration")
649             #with open("...", "w") as f:
650             #f.write("bla bla bal")
651             grub_config_file = open(grub_target + 'menu.lst', 'w')
652             grub_config_file.write(generate_grub_config(grml_flavour))
653             grub_config_file.close()
654
655             logging.info("Generating syslinux configuration")
656             syslinux_cfg = syslinux_target + 'syslinux.cfg'
657
658             # install main configuration only *once*, no matter how many ISOs we have:
659             if os.path.isfile(syslinux_cfg):
660                 string = open(syslinux_cfg).readline()
661                 main_identifier = re.compile(".*main config generated at: %s.*" % re.escape(str(datestamp)))
662                 if not re.match(main_identifier, string):
663                     syslinux_config_file = open(syslinux_cfg, 'w')
664                     logging.info("Notice: grml flavour %s is being installed as the default booting system." % grml_flavour)
665                     syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
666                     syslinux_config_file.close()
667             else:
668                     syslinux_config_file = open(syslinux_cfg, 'w')
669                     syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, options.bootoptions))
670                     syslinux_config_file.close()
671
672             # install flavour specific configuration only *once* as well
673             # kind of ugly - I'm pretty sure this could be smoother...
674             flavour_config = True
675             if os.path.isfile(syslinux_cfg):
676                 string = open(syslinux_cfg).readlines()
677                 logging.info("Notice: you can boot flavour %s using '%s' on the commandline." % (grml_flavour, grml_flavour))
678                 flavour = re.compile("grml2usb for %s: %s" % (re.escape(grml_flavour), re.escape(str(datestamp))))
679                 for line in string:
680                     if flavour.match(line):
681                         flavour_config = False
682
683
684             if flavour_config:
685                 syslinux_config_file = open(syslinux_cfg, 'a')
686                 syslinux_config_file.write(generate_flavour_specific_syslinux_config(grml_flavour, options.bootoptions))
687                 syslinux_config_file.close( )
688
689             logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
690             isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
691             isolinux_splash.write(generate_isolinux_splash(grml_flavour))
692             isolinux_splash.close( )
693
694
695     # make sure we sync filesystems before returning
696     proc = subprocess.Popen(["sync"])
697     proc.wait()
698
699
700 def uninstall_files(device):
701     """Get rid of all grml files on specified device
702
703     @device: partition where grml2usb files should be removed from"""
704
705     # TODO
706     logging.critical("TODO: uninstalling files from %s not yet implement, sorry." % device)
707
708
709 def identify_grml_flavour(mountpath):
710     """Get name of grml flavour
711
712     @mountpath: path where the grml ISO is mounted to
713     @return: name of grml-flavour"""
714
715     version_file = search_file('grml-version', mountpath)
716
717     if version_file == "":
718         logging.critical("Error: could not find grml-version file.")
719         raise
720
721     try:
722         tmpfile = open(version_file, 'r')
723         grml_info = tmpfile.readline()
724         grml_flavour = re.match(r'[\w-]*', grml_info).group()
725     except TypeError:
726         raise
727     except:
728         logging.critical("Unexpected error:", sys.exc_info()[0])
729         raise
730
731     return grml_flavour
732
733
734 def handle_iso(iso, device):
735     """Main logic for mounting ISOs and copying files.
736
737     @iso: full path to the ISO that should be installed to the specified device
738     @device: partition where the specified ISO should be installed to"""
739
740     logging.info("Using ISO %s" % iso)
741
742     if os.path.isdir(iso):
743         logging.critical("TODO: /live/image handling not yet implemented - sorry") # TODO
744         sys.exit(1)
745     else:
746         iso_mountpoint = tempfile.mkdtemp()
747         register_tmpfile(iso_mountpoint)
748         remove_iso_mountpoint = True
749
750         if not os.path.isfile(iso):
751             logging.critical("Fatal: specified ISO %s could not be read" % iso)
752             cleanup()
753             sys.exit(1)
754
755         mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
756
757         if os.path.isdir(device):
758             logging.info("Specified target is a directory, not mounting therefore.")
759             device_mountpoint = device
760             remove_device_mountpoint = False
761             skip_mbr = True
762
763         else:
764             device_mountpoint = tempfile.mkdtemp()
765             register_tmpfile(device_mountpoint)
766             remove_device_mountpoint = True
767             try:
768                 mount(device, device_mountpoint, "")
769             except Exception, error:
770                 logging.critical("Fatal: %s" % error)
771                 cleanup()
772
773         try:
774             grml_flavour = identify_grml_flavour(iso_mountpoint)
775             logging.info("Identified grml flavour \"%s\"." % grml_flavour)
776             copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint)
777         except TypeError:
778             logging.critical("Fatal: a critical error happend during execution, giving up")
779             sys.exit(1)
780         finally:
781             if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
782                 unmount(iso_mountpoint, "")
783
784                 os.rmdir(iso_mountpoint)
785                 unregister_tmpfile(iso_mountpoint)
786
787             if remove_device_mountpoint:
788                 unmount(device_mountpoint, "")
789
790                 if os.path.isdir(device_mountpoint):
791                     os.rmdir(device_mountpoint)
792                     unregister_tmpfile(device_mountpoint)
793
794         # grml_flavour_short = grml_flavour.replace('-','')
795         # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
796
797
798 def main():
799     """Main function [make pylint happy :)]"""
800
801     if options.version:
802         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
803         sys.exit(0)
804
805     if len(args) < 2:
806         parser.error("invalid usage")
807
808     # log handling
809     if options.verbose:
810         FORMAT = "%(asctime)-15s %(message)s"
811         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
812     elif options.quiet:
813         FORMAT = "Critial: %(message)s"
814         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
815     else:
816         FORMAT = "Info: %(message)s"
817         logging.basicConfig(level=logging.INFO, format=FORMAT)
818
819     if options.dryrun:
820         logging.info("Running in simulate mode as requested via option dry-run.")
821     else:
822         check_uid_root()
823
824     # specified arguments
825     device = args[len(args) - 1]
826     isos = args[0:len(args) - 1]
827
828     # make sure we can replace old grml2usb script and warn user when using old way of life:
829     if device.startswith("/mnt/external") or device.startswith("/mnt/usb") and not options.force:
830         print "Warning: the semantics of grml2usb has changed."
831         print "Instead of using grml2usb /path/to/iso %s you might" % device
832         print "want to use grml2usb /path/to/iso /dev/... instead."
833         print "Please check out the grml2usb manpage for details."
834         f = raw_input("Do you really want to continue? y/N ")
835         if f == "y" or f == "Y":
836            pass
837         else:
838             sys.exit(1)
839
840     # make sure we have syslinux available
841     if options.mbr:
842         if not which("syslinux") and not options.copyonly and not options.dryrun:
843             logging.critical('Sorry, syslinux not available. Exiting.')
844             logging.critical('Please install syslinux or consider using the --grub option.')
845             sys.exit(1)
846
847     # make sure we have mkfs.vfat available
848     if options.fat16 and not options.force:
849         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
850             logging.critical('Sorry, mkfs.vfat not available. Exiting.')
851             logging.critical('Please make sure to install dosfstools.')
852             sys.exit(1)
853
854         # make sure the user is aware of what he is doing
855         f = raw_input("Are you sure you want to format the device with a fat16 filesystem? y/N ")
856         if f == "y" or f == "Y":
857             logging.info("Note: you can skip this question using the option --force")
858             mkfs_fat16(device)
859         else:
860             sys.exit(1)
861
862     # check for vfat filesystem
863     if device is not None and not os.path.isdir(device):
864         try:
865             check_for_fat(device)
866         except Exception, error:
867             logging.critical("Execution failed: %s", error)
868             sys.exit(1)
869
870     if not check_for_usbdevice(device):
871         print "Warning: the specified device %s does not look like a removable usb device." % device
872         f = raw_input("Do you really want to continue? y/N ")
873         if f == "y" or f == "Y":
874            pass
875         else:
876             sys.exit(1)
877
878     # format partition:
879     if options.fat16:
880         mkfs_fat16(device)
881
882     # main operation (like installing files)
883     for iso in isos:
884         handle_iso(iso, device)
885
886     # install MBR
887     if not options.mbr or skip_mbr:
888         logging.info("You are NOT using the --mbr option. Consider using it if your device does not boot.")
889     else:
890         # make sure we install MBR on /dev/sdX and not /dev/sdX#
891         if device[-1:].isdigit():
892             mbr_device = re.match(r'(.*?)\d*$', device).group(1)
893
894         try:
895             install_mbr(mbr_device)
896         except IOError, error:
897             logging.critical("Execution failed: %s", error)
898             sys.exit(1)
899         except Exception, error:
900             logging.critical("Execution failed: %s", error)
901             sys.exit(1)
902
903     # Install bootloader only if not using the --copy-only option
904     if options.copyonly:
905         logging.info("Not installing bootloader and its files as requested via option copyonly.")
906     else:
907         install_bootloader(device)
908
909     # finally be politely :)
910     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
911
912
913 if __name__ == "__main__":
914     try:
915         main()
916     except KeyboardInterrupt:
917         logging.info("Received KeyboardInterrupt")
918         cleanup()
919
920 ## END OF FILE #################################################################
921 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8