1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """
21 Our main LiveUSBCreator module.
22
23 This contains the LiveUSBCreator parent class, which is an abstract interface
24 that provides platform-independent methods. Platform specific implementations
25 include the LinuxLiveUSBCreator and the WindowsLiveUSBCreator.
26 """
27
28 import subprocess
29 import tempfile
30 import logging
31 import shutil
32 import sha
33 import os
34 import re
35
36 from StringIO import StringIO
37 from datetime import datetime
38 from stat import ST_SIZE
39
40 from liveusb.releases import releases
41 from liveusb import _
42
43
45 """ A generic error message that is thrown by the LiveUSBCreator """
46
47
49 """ An OS-independent parent class for Live USB Creators """
50
51 iso = None
52 label = "FEDORA"
53 fstype = None
54 drives = {}
55 overlay = 0
56 dest = None
57 uuid = None
58 pids = []
59 output = StringIO()
60 totalsize = 0
61 isosize = 0
62 _drive = None
63 mb_per_sec = 0
64 log = None
65
66 drive = property(fget=lambda self: self.drives[self._drive],
67 fset=lambda self, d: self._set_drive(d))
68
72
74 self.log = logging.getLogger(__name__)
75 level = logging.INFO
76 if self.opts.verbose:
77 level = logging.DEBUG
78 self.log.setLevel(level)
79 handler = logging.StreamHandler()
80 handler.setLevel(level)
81 formatter = logging.Formatter("[%(module)s:%(lineno)s] %(message)s")
82 handler.setFormatter(formatter)
83 self.log.addHandler(handler)
84
86 """ This method should populate self.drives with removable devices """
87 raise NotImplementedError
88
90 """
91 Verify the filesystem of our device, setting the volume label
92 if necessary. If something is not right, this method throws a
93 LiveUSBError.
94 """
95 raise NotImplementedError
96
98 """ Return the number of free bytes on a given drive.
99
100 If drive is None, then use the currently selected device.
101 """
102 raise NotImplementedError
103
105 """ Extract the LiveCD ISO to the USB drive """
106 raise NotImplementedError
107
109 """ Verify the MD5 checksum of the ISO """
110 raise NotImplementedError
111
113 """ Install the bootloader to our device.
114
115 Platform-specific classes inheriting from the LiveUSBCreator are
116 expected to implement this method to install the bootloader to the
117 specified device using syslinux. This specific implemention is
118 platform independent and performs sanity checking along with adding
119 OLPC support.
120 """
121 if not os.path.exists(os.path.join(self.dest, 'isolinux')):
122 raise LiveUSBError('extract_iso must be run before '
123 'install_bootloader')
124 if self.opts.xo:
125 self._setup_olpc()
126
128 """ Install the Open Firmware configuration for the OLPC.
129
130 This method will make the selected device bootable on the OLPC. It
131 does this by installing a /boot/olpc.fth open firmware configuration
132 file that enables booting off of USB and SD cards on the XO.
133 """
134 self.log.info(_('Setting up OLPC boot file...'))
135 args = []
136
137
138 cfg = file(os.path.join(self.dest, 'isolinux', 'syslinux.cfg'))
139 for line in cfg.readlines():
140 if 'append' in line:
141 args.extend([arg for arg in line.split()[1:]
142 if not arg.startswith('initrd')])
143 break
144 cfg.close()
145
146 from liveusb.olpc import ofw_config
147 if not os.path.exists(os.path.join(self.dest, 'boot')):
148 os.mkdir(os.path.join(self.dest, 'boot'))
149 olpc_cfg = file(os.path.join(self.dest, 'boot', 'olpc.fth'), 'w')
150 olpc_cfg.write(ofw_config % ' '.join(args))
151 olpc_cfg.close()
152 self.log.debug('Wrote %s' % olpc_cfg.name)
153
155 """ Terminate any subprocesses that we have spawned """
156 raise NotImplementedError
157
159 """ Mount self.drive, setting the mount point to self.mount """
160 raise NotImplementedError
161
163 """ Unmount the device mounted at self.mount """
164 raise NotImplementedError
165
166 - def popen(self, cmd, **kwargs):
167 """ A wrapper method for running subprocesses.
168
169 This method handles logging of the command and it's output, and keeps
170 track of the pids in case we need to kill them. If something goes
171 wrong, an error log is written out and a LiveUSBError is thrown.
172
173 @param cmd: The commandline to execute. Either a string or a list.
174 @param kwargs: Extra arguments to pass to subprocess.Popen
175 """
176 self.log.debug(cmd)
177 self.output.write(cmd)
178 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
179 stderr=subprocess.PIPE, stdin=subprocess.PIPE,
180 shell=True, **kwargs)
181 self.pids.append(proc.pid)
182 out, err = proc.communicate()
183 self.output.write(out + '\n' + err + '\n')
184 if proc.returncode:
185 self.write_log()
186 raise LiveUSBError(_("There was a problem executing the following "
187 "command: `%s`\nA more detailed error log has "
188 "been written to 'liveusb-creator.log'" % cmd))
189 return proc
190
192 """ Verify the SHA1 checksum of our ISO if it is in our release list """
193 self.log.info(_("Verifying SHA1 of LiveCD image..."))
194 if not progress:
195 class DummyProgress:
196 def set_max_progress(self, value): pass
197 def update_progress(self, value): pass
198 progress = DummyProgress()
199 release = self.get_release_from_iso()
200 if release:
201 progress.set_max_progress(self.isosize / 1024)
202 checksum = sha.new()
203 isofile = file(self.iso, 'rb')
204 bytes = 1024**2
205 total = 0
206 while bytes:
207 data = isofile.read(bytes)
208 checksum.update(data)
209 bytes = len(data)
210 total += bytes
211 progress.update_progress(total / 1024)
212 if checksum.hexdigest() == release['sha1']:
213 return True
214 else:
215 self.log.info(_("Error: The SHA1 of your Live CD is "
216 "invalid. You can run this program with "
217 "the --noverify argument to bypass this "
218 "verification check."))
219 return False
220 else:
221 self.log.debug(_('Unknown ISO, skipping checksum verification'))
222
224 """ Make sure there is enough space for the LiveOS and overlay """
225 freebytes = self.get_free_bytes()
226 self.log.debug('freebytes = %d' % freebytes)
227 self.isosize = os.stat(self.iso)[ST_SIZE]
228 self.log.debug('isosize = %d' % self.isosize)
229 overlaysize = self.overlay * 1024**2
230 self.log.debug('overlaysize = %d' % overlaysize)
231 self.totalsize = overlaysize + self.isosize
232 if self.totalsize > freebytes:
233 raise LiveUSBError(_("Not enough free space on device." +
234 "\n%dMB ISO + %dMB overlay > %dMB free space" %
235 (self.isosize/1024**2, self.overlay,
236 freebytes/1024**2)))
237
249
251 """ Generate our syslinux.cfg """
252 isolinux = file(os.path.join(self.dest, "isolinux", "isolinux.cfg"),'r')
253 syslinux = file(os.path.join(self.dest, "isolinux", "syslinux.cfg"),'w')
254 usblabel = self.uuid and 'UUID=' + self.uuid or 'LABEL=' + self.label
255 for line in isolinux.readlines():
256 if "CDLABEL" in line:
257 line = re.sub("CDLABEL=[^ ]*", usblabel, line)
258 line = re.sub("rootfstype=[^ ]*",
259 "rootfstype=%s" % self.fstype,
260 line)
261 if self.overlay and "liveimg" in line:
262 line = line.replace("liveimg", "liveimg overlay=" + usblabel)
263 line = line.replace(" ro ", " rw ")
264 if self.opts.kernel_args:
265 line = line.replace("liveimg", "liveimg %s" %
266 ' '.join(self.opts.kernel_args.split(',')))
267 syslinux.write(line)
268 isolinux.close()
269 syslinux.close()
270
272 """ Delete the existing LiveOS """
273 self.log.info(_('Removing existing Live OS'))
274 for path in [self.get_liveos(),
275 os.path.join(self.dest + os.path.sep, 'syslinux'),
276 os.path.join(self.dest + os.path.sep, 'isolinux')]:
277 if os.path.exists(path):
278 self.log.debug("Deleting " + path)
279 try:
280 shutil.rmtree(path)
281 except OSError, e:
282 raise LiveUSBError(_("Unable to remove previous LiveOS: "
283 "%s" % str(e)))
284
286 """ Write out our subprocess stdout/stderr to a log file """
287 out = file('liveusb-creator.log', 'a')
288 out.write(self.output.getvalue())
289 out.close()
290
293
295 return os.path.join(self.dest + os.path.sep, "LiveOS")
296
299
301 return os.path.join(self.get_liveos(),
302 'overlay-%s-%s' % (self.label, self.uuid or ''))
303
305 """ If the ISO is for a known release, return it. """
306 isoname = os.path.basename(self.iso)
307 for release in releases:
308 if os.path.basename(release['url']) == isoname:
309 return release
310
318
320 """ Return a dictionary of proxy settings """
321 return None
322
323
325
326 bus = None
327 hal = None
328
330 """ Detect all removable USB storage devices using HAL via D-Bus """
331 import dbus
332 self.drives = {}
333 self.bus = dbus.SystemBus()
334 hal_obj = self.bus.get_object("org.freedesktop.Hal",
335 "/org/freedesktop/Hal/Manager")
336 self.hal = dbus.Interface(hal_obj, "org.freedesktop.Hal.Manager")
337
338 devices = []
339 if self.opts.force:
340 devices = self.hal.FindDeviceStringMatch('block.device',
341 self.opts.force)
342 else:
343 devices = self.hal.FindDeviceByCapability("storage")
344
345 for device in devices:
346 dev = self._get_device(device)
347 if self.opts.force or dev.GetProperty("storage.bus") == "usb" and \
348 dev.GetProperty("storage.removable"):
349 if dev.GetProperty("block.is_volume"):
350 self._add_device(dev)
351 continue
352 else:
353 children = self.hal.FindDeviceStringMatch("info.parent",
354 device)
355 for child in children:
356 child = self._get_device(child)
357 if child.GetProperty("block.is_volume"):
358 self._add_device(child, parent=dev)
359 break
360
361 if not len(self.drives):
362 raise LiveUSBError(_("Unable to find any USB drives"))
363
365 mount = str(dev.GetProperty('volume.mount_point'))
366 device = str(dev.GetProperty('block.device'))
367 self.drives[device] = {
368 'label' : str(dev.GetProperty('volume.label')).replace(' ', '_'),
369 'fstype' : str(dev.GetProperty('volume.fstype')),
370 'uuid' : str(dev.GetProperty('volume.uuid')),
371 'mount' : mount,
372 'udi' : dev,
373 'unmount' : False,
374 'free' : mount and self.get_free_bytes(mount) / 1024**2 or None,
375 'device' : device,
376 'parent' : parent.GetProperty('block.device')
377 }
378
380 """ Mount our device with HAL if it is not already mounted """
381 import dbus
382 self.dest = self.drive['mount']
383 if not self.dest:
384 if not self.fstype:
385 raise LiveUSBError(_("Filesystem for %s unknown!" %
386 self.drive['device']))
387 try:
388 self.log.debug("Calling %s.Mount('', %s, [], ...)" % (
389 self.drive['udi'], self.fstype))
390 self.drive['udi'].Mount('', self.fstype, [],
391 dbus_interface='org.freedesktop.Hal.Device.Volume')
392 self.drive['unmount'] = True
393 except dbus.exceptions.DBusException, e:
394 if e.get_dbus_name() == \
395 'org.freedesktop.Hal.Device.Volume.AlreadyMounted':
396 self.log.debug('Device already mounted')
397 except Exception, e:
398 raise LiveUSBError(_("Unable to mount device: %s" % str(e)))
399 device = self.hal.FindDeviceStringMatch('block.device',
400 self.drive['device'])
401 device = self._get_device(device[0])
402 self.dest = device.GetProperty('volume.mount_point')
403 self.log.debug("Mounted %s to %s " % (self.drive['device'],
404 self.dest))
405 self.drive['mount'] = self.dest
406 else:
407 self.log.debug("Using existing mount: %s" % self.dest)
408
410 """ Unmount our device if we mounted it to begin with """
411 import dbus
412 try:
413 unmount = self.drive.get('unmount')
414 except KeyError:
415 return
416 if self.dest and unmount:
417 self.log.debug("Unmounting %s from %s" % (self.drive['device'],
418 self.dest))
419 try:
420 self.drive['udi'].Unmount([],
421 dbus_interface='org.freedesktop.Hal.Device.Volume')
422 except dbus.exceptions.DBusException, e:
423 raise
424 self.log.warning("Unable to unmount device: %s" % str(e))
425 return
426 self.drive['unmount'] = False
427 self.drive['mount'] = None
428 if os.path.exists(self.dest):
429 self.log.error("Mount %s exists after unmounting" % self.dest)
430
431 self.dest = None
432
434 self.log.info(_("Verifying filesystem..."))
435 if self.fstype not in ('vfat', 'msdos', 'ext2', 'ext3'):
436 if not self.fstype:
437 raise LiveUSBError(_("Unknown filesystem for %s. Your device "
438 "may need to be reformatted."))
439 else:
440 raise LiveUSBError(_("Unsupported filesystem: %s" %
441 self.fstype))
442 if self.drive['label']:
443 self.label = self.drive['label']
444 else:
445 self.log.info("Setting %s label to %s" % (self.drive['device'],
446 self.label))
447 try:
448 if self.fstype in ('vfat', 'msdos'):
449 try:
450 self.popen('/sbin/dosfslabel %s %s' % (
451 self.drive['device'], self.label))
452 except LiveUSBError:
453
454 pass
455 else:
456 self.popen('/sbin/e2label %s %s' % (self.drive['device'],
457 self.label))
458 except LiveUSBError, e:
459 self.log.error("Unable to change volume label: %s" % str(e))
460 self.label = None
461
463 """ Extract self.iso to self.dest """
464 self.log.info(_("Extracting live image to USB device..."))
465 tmpdir = tempfile.mkdtemp()
466 self.popen('mount -o loop,ro "%s" %s' % (self.iso, tmpdir))
467 tmpliveos = os.path.join(tmpdir, 'LiveOS')
468 try:
469 if not os.path.isdir(tmpliveos):
470 raise LiveUSBError(_("Unable to find LiveOS on ISO"))
471 liveos = os.path.join(self.dest, 'LiveOS')
472 if not os.path.exists(liveos):
473 os.mkdir(liveos)
474 for img in ('squashfs.img', 'osmin.img'):
475 start = datetime.now()
476 self.popen("cp %s '%s'" % (os.path.join(tmpliveos, img),
477 os.path.join(liveos, img)))
478 delta = datetime.now() - start
479 if delta.seconds:
480 self.mb_per_sec = (self.isosize / delta.seconds) / 1024**2
481 self.log.info(_("Wrote to device at") + " %d MB/sec" %
482 self.mb_per_sec)
483 isolinux = os.path.join(self.dest, 'isolinux')
484 if not os.path.exists(isolinux):
485 os.mkdir(isolinux)
486 self.popen("cp %s/* '%s'" % (os.path.join(tmpdir, 'isolinux'),
487 isolinux))
488 finally:
489 self.popen('umount %s' % tmpdir)
490
492 """ Run syslinux to install the bootloader on our devices """
493 LiveUSBCreator.install_bootloader(self)
494 self.log.info(_("Installing bootloader..."))
495 shutil.move(os.path.join(self.dest, "isolinux"),
496 os.path.join(self.dest, "syslinux"))
497 os.unlink(os.path.join(self.dest, "syslinux", "isolinux.cfg"))
498 self.popen('syslinux%s%s -d %s %s' % (self.opts.force and ' -f' or ' ',
499 self.opts.safe and ' -s' or ' ',
500 os.path.join(self.dest, 'syslinux'), self.drive['device']))
501
503 """ Return the number of available bytes on our device """
504 import statvfs
505 device = device and device or self.dest
506 stat = os.statvfs(device)
507 return stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
508
510 """ Return a dbus Interface to a specific HAL device UDI """
511 import dbus
512 dev_obj = self.bus.get_object("org.freedesktop.Hal", udi)
513 return dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
514
516 import signal
517 self.log.info("Cleaning up...")
518 for pid in self.pids:
519 try:
520 os.kill(pid, signal.SIGHUP)
521 self.log.debug("Killed process %d" % pid)
522 except OSError:
523 pass
524 self.unmount_device()
525
527 """ Verify the ISO md5sum.
528
529 At the moment this is Linux specific, until we port checkisomd5
530 to Windows.
531 """
532 self.log.info(_('Verifying ISO MD5 checksum'))
533 try:
534 self.popen('checkisomd5 "%s"' % self.iso)
535 except LiveUSBError:
536 return False
537 return True
538
540 """ Return the proxy settings.
541
542 At the moment this implementation only works on KDE, and should
543 eventually be expanded to support other platforms as well.
544 """
545 try:
546 from PyQt4 import QtCore
547 except ImportError:
548 self.log.warning("PyQt4 module not installed; skipping KDE "
549 "proxy detection")
550 return
551 kioslaverc = QtCore.QDir.homePath() + '/.kde/share/config/kioslaverc'
552 if not QtCore.QFile.exists(kioslaverc):
553 return {}
554 settings = QtCore.QSettings(kioslaverc, QtCore.QSettings.IniFormat)
555 settings.beginGroup('Proxy Settings')
556 proxies = {}
557
558 if settings.value('ProxyType').toInt()[0] == 1:
559 httpProxy = settings.value('httpProxy').toString()
560 if httpProxy != '':
561 proxies['http'] = httpProxy
562 ftpProxy = settings.value('ftpProxy').toString()
563 if ftpProxy != '':
564 proxies['ftp'] = ftpProxy
565 return proxies
566
567
569
571 import win32file, win32api
572 self.drives = {}
573 for drive in [l + ':' for l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ']:
574 if win32file.GetDriveType(drive) == win32file.DRIVE_REMOVABLE or \
575 drive == self.opts.force:
576 try:
577 vol = win32api.GetVolumeInformation(drive)
578 label = vol[0]
579 except:
580 label = None
581 self.drives[drive] = {
582 'label': label,
583 'mount': drive,
584 'uuid': self._get_device_uuid(drive),
585 'free': self.get_free_bytes(drive) / 1024**2,
586 'fstype': 'vfat',
587 'device': drive,
588 }
589 if not len(self.drives):
590 raise LiveUSBError(_("Unable to find any removable devices"))
591
593 import win32api, win32file, pywintypes
594 self.log.info(_("Verifying filesystem..."))
595 try:
596 vol = win32api.GetVolumeInformation(self.drive['device'])
597 except Exception, e:
598 raise LiveUSBError(_("Make sure your USB key is plugged in and "
599 "formatted with the FAT filesystem"))
600 if vol[-1] not in ('FAT32', 'FAT'):
601 raise LiveUSBError(_("Unsupported filesystem: %s\nPlease backup "
602 "and format your USB key with the FAT "
603 "filesystem." % vol[-1]))
604 self.fstype = 'vfat'
605 if vol[0] == '':
606 try:
607 win32file.SetVolumeLabel(self.drive['device'], self.label)
608 self.log.debug("Set %s label to %s" % (self.drive['device'],
609 self.label))
610 except pywintypes.error, e:
611 self.log.warning("Unable to SetVolumeLabel: " + str(e))
612 self.label = None
613 else:
614 self.label = vol[0].replace(' ', '_')
615
617 """ Return the number of free bytes on our selected drive """
618 import win32file
619 device = device and device or self.drive['device']
620 try:
621 (spc, bps, fc, tc) = win32file.GetDiskFreeSpace(device)
622 except Exception, e:
623 self.log.error("Problem determining free space: %s" % str(e))
624 return 0
625 return fc * (spc * bps)
626
628 """ Extract our ISO with 7-zip directly to the USB key """
629 self.log.info(_("Extracting live image to USB device..."))
630 start = datetime.now()
631 self.popen('7z x "%s" -x![BOOT] -y -o%s' % (
632 self.iso, self.drive['device']))
633 delta = datetime.now() - start
634 if delta.seconds:
635 self.mb_per_sec = (self.isosize / delta.seconds) / 1024**2
636 self.log.info(_("Wrote to device at") + " %d MB/sec" %
637 self.mb_per_sec)
638
640 """ Run syslinux to install the bootloader on our device """
641 LiveUSBCreator.install_bootloader(self)
642 self.log.info(_("Installing bootloader"))
643 device = self.drive['device']
644 if os.path.isdir(os.path.join(device + os.path.sep, "syslinux")):
645 syslinuxdir = os.path.join(device + os.path.sep, "syslinux")
646
647
648 for f in os.listdir(syslinuxdir):
649 os.chmod(os.path.join(syslinuxdir, f), 0777)
650 shutil.rmtree(syslinuxdir)
651 shutil.move(os.path.join(device + os.path.sep, "isolinux"),
652 os.path.join(device + os.path.sep, "syslinux"))
653 os.unlink(os.path.join(device + os.path.sep, "syslinux",
654 "isolinux.cfg"))
655 self.popen('syslinux%s%s -m -a -d %s %s' % (self.opts.force and ' -f'
656 or ' ', self.opts.safe and ' -s' or ' ',
657 os.path.join(device + os.path.sep, 'syslinux'), device))
658
660 """ Return the UUID of our selected drive """
661 uuid = None
662 try:
663 import win32com.client
664 uuid = win32com.client.Dispatch("WbemScripting.SWbemLocator") \
665 .ConnectServer(".", "root\cimv2") \
666 .ExecQuery("Select VolumeSerialNumber from "
667 "Win32_LogicalDisk where Name = '%s'" %
668 drive)[0].VolumeSerialNumber
669 if uuid == 'None':
670 uuid = None
671 else:
672 uuid = uuid[:4] + '-' + uuid[4:]
673 self.log.debug("Found UUID %s for %s" % (uuid, drive))
674 except Exception, e:
675 self.log.warning("Exception while fetching UUID: %s" % str(e))
676 return uuid
677
678 - def popen(self, cmd, **kwargs):
679 import win32process
680 if isinstance(cmd, basestring):
681 cmd = cmd.split()
682 tool = os.path.join('tools', '%s.exe' % cmd[0])
683 if not os.path.exists(tool):
684 raise LiveUSBError(_("Cannot find") + ' %s. ' % (cmd[0]) +
685 _("Make sure to extract the entire "
686 "liveusb-creator zip file before "
687 "running this program."))
688 return LiveUSBCreator.popen(self, ' '.join([tool] + cmd[1:]),
689 creationflags=win32process.CREATE_NO_WINDOW,
690 **kwargs)
691
693 """ Terminate any subprocesses that we have spawned """
694 import win32api, win32con, pywintypes
695 for pid in self.pids:
696 try:
697 handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE,
698 False, pid)
699 self.log.debug("Terminating process %s" % pid)
700 win32api.TerminateProcess(handle, -2)
701 win32api.CloseHandle(handle)
702 except pywintypes.error:
703 pass
704
707
710
712 proxies = {}
713 try:
714 import _winreg as winreg
715 settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
716 'Software\\Microsoft\\Windows'
717 '\\CurrentVersion\\Internet Settings')
718 proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0]
719 if proxy:
720 server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0])
721 if ';' in server:
722 for p in server.split(';'):
723 protocol, address = p.split('=')
724 proxies[protocol] = '%s://%s' % (protocol, address)
725 else:
726 proxies['http'] = 'http://%s' % server
727 proxies['ftp'] = 'ftp://%s' % server
728 settings.Close()
729 except Exception, e:
730 self.log.warning('Unable to detect proxy settings: %s' % str(e))
731 self.log.debug('Using proxies: %s' % proxies)
732 return proxies
733
735 """ Verify the ISO md5sum.
736
737 At the moment this is Linux-only, until we port checkisomd5 to Windows.
738 """
739 pass
740