From 364915c890336499796d0ac2d43b5a2f906eb2b9 Mon Sep 17 00:00:00 2001
From: moebius/ALUG <fs11linux.20.masuefke@spamgourmet.com>
Date: Thu, 26 Mar 2015 15:25:38 +0100
Subject: [PATCH] Mass Storage Cloner for writing many USB sticks - 1st version

---
 MassStorageCloner/.gitignore        |   1 +
 MassStorageCloner/README            |  16 ++
 MassStorageCloner/StorageObjects.py | 343 ++++++++++++++++++++++++
 MassStorageCloner/StoreObjFrames.py | 291 +++++++++++++++++++++
 MassStorageCloner/blkwrite.sh       |  69 +++++
 MassStorageCloner/dbusdetect.py     | 207 +++++++++++++++
 MassStorageCloner/gui.py            | 391 ++++++++++++++++++++++++++++
 MassStorageCloner/vtk.py            | 223 ++++++++++++++++
 MassStorageCloner/write.sh          |  15 ++
 9 files changed, 1556 insertions(+)
 create mode 100644 MassStorageCloner/.gitignore
 create mode 100644 MassStorageCloner/README
 create mode 100644 MassStorageCloner/StorageObjects.py
 create mode 100644 MassStorageCloner/StoreObjFrames.py
 create mode 100755 MassStorageCloner/blkwrite.sh
 create mode 100644 MassStorageCloner/dbusdetect.py
 create mode 100755 MassStorageCloner/gui.py
 create mode 100644 MassStorageCloner/vtk.py
 create mode 100755 MassStorageCloner/write.sh

diff --git a/MassStorageCloner/.gitignore b/MassStorageCloner/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/MassStorageCloner/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/MassStorageCloner/README b/MassStorageCloner/README
new file mode 100644
index 0000000..1536920
--- /dev/null
+++ b/MassStorageCloner/README
@@ -0,0 +1,16 @@
+Mass Storage Cloner
+(C)2014-2015 Martin Suefke
+License: GPLv3 or newer, the GNU Public Licence, see http://www.gnu.org/licenses/gpl.html
+
+Use this tool on a linux machine to write any image file to a target device selected in a GUI.
+
+Prerequisites:
+python 2.6 + Tk
+xterm
+pv
+dd
+
+Select the source file by editing 'write.sh'
+Start the program with 'python gui.py'
+
+First release 2015-03-26, version history in 'gui.py'
diff --git a/MassStorageCloner/StorageObjects.py b/MassStorageCloner/StorageObjects.py
new file mode 100644
index 0000000..7b163f4
--- /dev/null
+++ b/MassStorageCloner/StorageObjects.py
@@ -0,0 +1,343 @@
+# -*- coding: utf-8 -*-
+
+
+"""
+ Storage Abstraction in Python Classes
+ (C) 2014 Martin Süfke
+ License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html
+"""
+# Proper logging
+from pprint import pformat
+import logging
+
+if __name__ == "__main__":
+  #logging.basicConfig(level=logging.WARN)
+  raise Exception("{0} cannot run as the main python program".format(__file__))
+
+if len(logging.root.handlers) == 0:
+# SPE compatible logging format
+  logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d')
+logger = logging.getLogger(__name__)
+
+import inspect
+
+from dbus import String as _dbus_String
+from dbus import Array as _dbus_Array
+from dbus import DBusException
+
+def Implement(msg=""):
+  logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4])
+
+def DBusStringArrayToString(Array_Or_String):
+  if isinstance(Array_Or_String, _dbus_Array):
+    return ''.join(chr(c) for c in Array_Or_String if c <> 0).strip()
+  elif isinstance(Array_Or_String, str):
+    return Array_Or_String.rstrip('\x00').strip()
+
+class Storage(object):
+  """ Represents system data storage
+      may have multiple StorageDrives
+  """
+  def __init__(self):
+    self._ChildClass = StorageDrive
+    self.StorageDrives = {}
+    
+  def AddDrive(self, UDisks2Path, UDisks2Dict, EjectFunc, PowerOffFunc):
+    if UDisks2Dict is None:
+      logger.error("AddDrive() Empty dictionary for %s",UDisks2Path)
+      return
+    self.StorageDrives[UDisks2Path] = self._ChildClass(self, UDisks2Path, UDisks2Dict, EjectFunc, PowerOffFunc)
+
+  def RemoveDrive(self, UDisks2Path):
+    Drive = self.StorageDrives.pop(UDisks2Path, None)
+    if Drive is not None:
+      Drive._close()
+    else:
+      logger.warn("No Drive by Path %s", UDisks2Path)
+
+  def AddBlockDevice(self, UDisks2Path, UDisks2Dict):
+    logger.debug('Storage U2d.k: %s', UDisks2Dict.keys())
+    drive = UDisks2Dict[_dbus_String(u'Drive')]
+    self.StorageDrives[drive].AddBlockDevice(UDisks2Path, UDisks2Dict)
+
+  def RemoveBlockDevice(self, UDisks2Path):
+    BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path)
+    if BlockDev is not None:
+      Drive = BlockDev.Parent
+      Drive.RemoveBlockDevice(UDisks2Path)
+    else:
+      logger.warn("No BlockDevice by Path %s", UDisks2Path)
+
+  def AddFileSystem(self, UDisks2Path, UDisks2Dict, MountFunc, UnmountFunc):
+    if UDisks2Dict is None:
+      logger.info("AddFileSystem() Empty dictionary for %s",UDisks2Path)
+      return
+    #logger.debug("UDisks2Path = %s, Dict = %s)",UDisks2Path, UDisks2Dict)
+    BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path)
+    if BlockDev is None:
+      logger.warn("AddFileSystem( UDisks2Path = %s, Dict = %s) No BlockDevice",UDisks2Path, UDisks2Dict)
+    else:
+      BlockDev.AddFileSystem(UDisks2Path, UDisks2Dict, MountFunc, UnmountFunc)
+
+  def RemoveFileSystem(self, UDisks2Path):
+    BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path)
+    if BlockDev is not None:
+      BlockDev.RemoveFileSystem(UDisks2Path)
+    else:
+      logger.warn("No BlockDevice for FileSytem Path %s", UDisks2Path)
+
+  def UpdateMountPoints(self, UDisks2Path, UDisk2Mountpoints, UnmountFunc):
+    """Update all Filesystems matched by UDisks2Path to reflect (new) UDisk2Mountpoints"""
+    BlockDev = self.GetStorageBlockDeviceByPath(UDisks2Path)
+    if BlockDev is None:
+      logger.warn("UDisks2Path %s: No such BlockDevice",UDisks2Path)
+    else:
+      BlockDev.UpdateMountPoints(UDisks2Path, UDisk2Mountpoints, UnmountFunc)
+        
+  def GetStorageBlockDeviceByPath(self, aUDisks2Path):
+    """ Walk through all StorageDrives and look for a StorageBlockDevice with given UDisks2Path
+        Returns the BlockDevice
+    """
+    for drive in self.StorageDrives.itervalues():
+      blkdev = drive.GetStorageBlockDeviceByPath(aUDisks2Path)
+      if blkdev is not None:
+        return blkdev
+    return None
+
+class StorageDrive(object):
+  """ Represents a physical drive/device/medium
+      parent is Storage
+      may have multiple StorageBlockDevices
+      can Eject() -> will not detect drive any more until physical (?) reconnect
+  """
+  def __init__(self, Parent, UDisks2Path, UDisks2Dict, EjectFunc, PowerOffFunc):
+    self._ChildClass = StorageBlockDevice
+    self.Parent = Parent
+    self.UDisks2Path = UDisks2Path
+    self.UDisks2Dict = UDisks2Dict
+    self.EjectFunc = EjectFunc
+    self.PowerOffFunc = PowerOffFunc
+    for name in "Id Size".split():
+      setattr(self, name, UDisks2Dict.get(_dbus_String(unicode(name))))
+    self.BlockDevices = {}
+    
+  def _close(self):
+    """ a manual destructor """
+    pass
+  
+  def AddBlockDevice(self, UDisks2Path, UDisks2Dict):
+    if UDisks2Dict is None:
+      logger.error("AddBlockDevice() Empty dictionary for %s",UDisks2Path)
+      return
+    #logger.debug("AddBlockDevice( Path = %s, Dict.Keys = %s)",  UDisks2Path, UDisks2Dict.keys())
+    blockdev = self._ChildClass(self, UDisks2Path, UDisks2Dict)
+    self.BlockDevices[UDisks2Path] = blockdev
+
+  def RemoveBlockDevice(self, UDisks2Path):
+    BlkDev = self.BlockDevices.pop(UDisks2Path, None)
+    if BlkDev is not None:
+      BlkDev._close()
+    else:
+      logger.warn("No BlockDevice by Path %s", UDisks2Path)
+
+  def GetStorageBlockDeviceByPath(self, aUDisks2Path):
+    """ Get StorageBlockDevice by UDisks2Path """
+    return self.BlockDevices.get(aUDisks2Path)
+
+  def Eject(self):
+    logger.debug("StorageDrive.Eject() on %s",self.UDisks2Path)
+    if callable(self.EjectFunc):
+      self.EjectFunc(self.UDisks2Path)
+    else:
+      logger.error("StorageDrive.Eject has uncallable EjectFunc")
+
+  def PowerOff(self):
+    logger.debug("StorageDrive.Poweroff() on %s",self.UDisks2Path)
+    if callable(self.PowerOffFunc):
+      self.PowerOffFunc(self.UDisks2Path)
+    else:
+      logger.error("StorageDrive.PowerOff has uncallable PowerOffFunc")
+
+    
+  def __str__(self):
+    return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.UDisks2Path)
+    
+class StorageBlockDevice(object):
+  """ Represents a block device '/dev/sdXYZ'
+      parent is StorageDrive
+      may have a single StorageFileSystem 
+      can ReScan() -> does nothing (?)
+  """
+  def __init__(self, Parent, UDisks2Path, UDisks2Dict=None, ChildClass=None):
+    assert UDisks2Dict is not None, "StorageBlockDevice( UDisks2Path = {0:s} ), UDisks2Dict is None. ".format(UDisks2Path)
+    #Implement(pformat([UDisks2Path, UDisks2Dict.keys()]))
+    #logger.debug("StorageBlockDevice.__init__(): ChildClass %s", ChildClass.__name__)
+    self._ChildClass = ChildClass or StorageFileSystem
+    self.Parent = Parent    
+    self.UDisks2Path = UDisks2Path
+    self.UDisks2Dict = UDisks2Dict
+    self.Size = UDisks2Dict.get(_dbus_String(u'Size'))
+    logger.debug('Getting PreferredDevice')
+    self.PreferredDevice = DBusStringArrayToString(UDisks2Dict.get(_dbus_String(u'PreferredDevice'),''))
+      
+    # Filesystem properties: IdUUID + IdLabel , IdUsage + IdType + IdVersion
+    # Crude hack: if IdUsage == Filesystem > "" then create a FS object 
+
+    # Even worse Hack for ease of use:
+    #  Leave Filesytem alone for now, add it when AddFileSystem() is called
+    self.FileSystem = None
+    self.FsParms = dict() # keep these
+    for name in "IdUUID IdLabel IdUsage IdType IdVersion".split():
+      self.FsParms[name] = str(UDisks2Dict.get(_dbus_String(unicode(name))).strip('\x00').strip())
+
+#    if len(self.FsParms['IdUsage'])>0:
+#      logger.info("got Filesystem info, not adding yet: %s using Class %s", 
+#                  pformat(self.FsParms),
+#                  self._ChildClass.__name__
+#                  )
+
+  def _close(self):
+    """ a manual destructor """
+    pass
+
+  def AddFileSystem(self, UDisks2Path, UDisks2Dict, MountFunc, UnmountFunc):
+    """ Add the info in the "Filesystem" dictionary or UDisks2.
+        That means the mount points, basically
+    """
+    if self.FileSystem is None:
+      self.FileSystem = self._ChildClass(self, MountFunc, **self.FsParms)
+      #logger.warn("Trying to AddFileSystemPoint() to StorageBlockDevice %s which has no (known) Filesystem",self.UDisks2Path)
+    #else:
+    self.FileSystem.AddMountPoints(UDisks2Path, UDisks2Dict, UnmountFunc)
+
+  def RemoveFileSystem(self, UDisks2Path):
+    if UDisks2Path == self.UDisks2Path:
+      if self.FileSystem is not None:
+        fs=self.FileSystem
+        self.FileSystem = None
+        fs._close()
+      else:
+        logger.warn("Trying to remove non-existent filesystem by Path %s", UDisks2Path)
+    else:
+      logger.warn("No FileSystem by Path %s", UDisks2Path)
+
+  def UpdateMountPoints(self, UDisks2Path, UDisk2Mountpoints, UnmountFunc):
+    """Update the BlockDevice's FileSystem to reflect (new) UDisk2Mountpoints"""
+    assert self.UDisks2Path == UDisks2Path, "UpdateMountPoints()  with wrong UDisks2Path, expected {0!s}, got {1!s}".format(self.UDisks2Path, UDisks2Path) 
+    assert self.FileSystem is not None, "UpdateMountPoints() FileSystem is None"
+    self.FileSystem.UpdateMountPoints(UDisks2Path, UDisk2Mountpoints, UnmountFunc)
+    
+  def ReScan(self):
+    Implement()
+
+  def __str__(self):
+    return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.UDisks2Path)
+
+class StorageFileSystem(object):
+  """ Represents a Filesystem (vfat, ext, ...)
+      parent is BlockDevice
+      may have multiply StorageMountpoint
+      can Mount()
+  """
+  def __init__(self, Parent, MountFunc, IdUUID, IdLabel, IdUsage, IdType, IdVersion):
+    self._ChildClass = StorageMountPoint
+    self.Parent = Parent
+    self.MountFunc = MountFunc # Call this to mount the filesystem
+    self.IdUUID = IdUUID
+    self.IdLabel = IdLabel
+    self.IdUsage = IdUsage
+    self.IdType = IdType
+    self.IdVersion = IdVersion
+    self.MountPoints = {}
+
+  def _close(self):
+    """ a manual destructor """
+    pass
+
+  def AddMountPoints(self, UDisks2Path, UDisks2Dict, UnmountFunc):
+    """ Add all MountPoints based on the UDisks2 Filesystem dictionary """
+    if UDisks2Path <> self.Parent.UDisks2Path:
+      logger.error("AddMountPoint() UDisks2Path given >%s< differs from Parent.Path >%s<.", UDisks2Path, self.Parent.UDisks2Path)
+      return
+    U2MountPoints = UDisks2Dict.get(_dbus_String(u'MountPoints'), {})
+    for U2MPoint in U2MountPoints:
+      MountPoint = ''.join(chr(c) for c in U2MPoint if c <> 0).strip()
+      #logger.debug("got Mountpoints %s", MountPoint)
+      self.AddMountPoint(UDisks2Path, MountPoint, UnmountFunc)
+
+  def AddMountPoint(self, UDisks2Path, MountPoint, UnmountFunc):
+    """ Add single MountPoint based on the UDisks2Path and the MountPoint directory """
+    MountPointObj = self._ChildClass(self, UDisks2Path, MountPoint, UnmountFunc)
+    self.MountPoints[MountPoint]=MountPointObj
+
+  def UpdateMountPoints(self, UDisks2Path, UDisk2Mountpoints, UnmountFunc):
+    """Update this FileSystem to reflect (new) UDisk2Mountpoints"""
+    if UDisks2Path <> self.Parent.UDisks2Path:
+      logger.error("UpdateMountPoints() UDisks2Path given >%s< differs from Parent.Path >%s<.", UDisks2Path, self.Parent.UDisks2Path)
+      return
+    NewMountpoints=list(s.strip('\x00') for s in UDisk2Mountpoints)
+    DelMountpoints=list()
+      
+    # First, kill all stale mountpoints
+    for oldmp in self.MountPoints: # keys are mount point paths
+      if oldmp in NewMountpoints:
+        logger.debug("known MP >%s<", oldmp)
+        NewMountpoints.remove(oldmp)        
+      else:
+        logger.debug("stale MP to remove >%s<", oldmp)
+        DelMountpoints.append(oldmp)
+
+    for delmp in DelMountpoints:
+      logger.debug("delete stale MP >%s<", delmp)
+      self.MountPoints.pop(delmp)._close()
+      # Now create new mountpoints
+    for newmp in NewMountpoints:
+      logger.debug("create new MP >%s<", newmp)
+      self.AddMountPoint(UDisks2Path, newmp, UnmountFunc)
+    
+  def __str__(self):
+    return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.Parent.UDisks2Path)
+
+  def Mount(self, *args, **kwargs):
+    logger.debug("StorageMountPoint.Mount args: %s %s",args, kwargs)
+    if callable(self.MountFunc):
+      try:
+        self.MountFunc(self.Parent.UDisks2Path)
+      except DBusException as dbe:
+        logger.warn(dbe.message)
+    else:
+      logger.error("StorageMountPoint.Mount has uncallable MountFunc")
+      
+class StorageMountPoint(object):
+  """ Represents a Filesystem (vfat, ext, ...)
+      parent is StorageFileSystem
+      can UnMount()
+  """
+  def __init__(self, Parent, UDisks2Path, MountPoint, UnmountFunc):
+    self.Parent = Parent
+    self.UDisks2Path = UDisks2Path
+    self.MountPoint = MountPoint
+    self.UnmountFunc = UnmountFunc
+    #Implement()
+
+#  def __del__(self):
+#    logger.debug("%s.__del__()",self.__class__.__name__)
+
+  def _close(self):
+    """ A 'manual' destructor"""
+    logger.debug("%s _close for %s",self.__class__.__name__,str(self))
+
+  def __str__(self):
+    return '{0:s}( UDisks2Path = "{1:s}")'.format(self.__class__.__name__, self.UDisks2Path)
+
+  def Unmount(self, *args, **kwargs):
+    logger.debug("StorageMountPoint.Unmount args: %s %s", args, kwargs)
+    if callable(self.UnmountFunc):
+      try:
+        self.UnmountFunc(self.UDisks2Path)
+      except DBusException as dbe:
+        logger.warn(dbe.message)
+    else:
+      logger.error("StorageMountPoint.Unmount has uncallable UnmountFunc")
+      
+#end;
diff --git a/MassStorageCloner/StoreObjFrames.py b/MassStorageCloner/StoreObjFrames.py
new file mode 100644
index 0000000..e216120
--- /dev/null
+++ b/MassStorageCloner/StoreObjFrames.py
@@ -0,0 +1,291 @@
+# -*- coding: utf-8 -*-
+
+"""
+ TK Frames for Representation of Storage Abstraction
+ (C) 2014 Martin Süfke
+ License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html
+"""
+# Proper logging
+from pprint import pprint, pformat
+import logging
+#import os         # A dirty hack here 
+import subprocess 
+import shlex
+import signal      # future
+
+
+if __name__ == "__main__":
+  #logging.basicConfig(level=logging.WARN)
+  raise Exception("{0} cannot run as the main python program".format(__file__))
+
+if len(logging.root.handlers) == 0:
+# SPE compatible logging format
+  logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d')
+logger = logging.getLogger(__name__)
+
+import inspect
+
+import StorageObjects
+
+import  Tkinter as Tk
+#from    Tkinter import BooleanVar, IntVar, DoubleVar, StringVar
+#import  tkMessageBox as dialog
+#import  tkFileDialog as filedialog
+from    Tkconstants import N, E, S, W, HORIZONTAL, LEFT, RIGHT, TOP, BOTTOM, \
+                           BOTH, NORMAL, DISABLED, X, Y
+
+def Implement(msg=""):
+  logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4])
+
+def SizeStr(size):
+  """ Print size with thousands separators """
+  return "{0:,d} bytes".format(size)
+    
+class TkFStorage(StorageObjects.Storage):
+  """ Tk Frame for Storage """
+  def __init__(self, ParentFrame):
+    #logger.debug(self.__class__.__name__)
+    super(self.__class__,self).__init__()
+    self._ChildClass = TkFDrive
+    self.TKFrame = Tk.Frame(ParentFrame, borderwidth=2, relief='groove')
+    self.TKFrame.pack(side=TOP, fill=X)
+    #Tk.Label(self.TKFrame,text=self.__class__.__name__).pack(side=LEFT, fill=X)    
+
+  def FilterViewDriveAdd(self, MoreFiltersAsDict):
+    """ Adds view filters on drives 
+      example: FilterViewDriveAdd(self, {'Removable': True})
+    """
+    #logger.debug("Num Drives %d", len(self.StorageDrives))
+    for k,v in self.StorageDrives.iteritems():
+#      logger.debug("\nDrive %s %s", k, str(v))
+      v.FilterViewAdd(MoreFiltersAsDict)
+# TODO: Idea here is to retain the order of hidden/visible drives. 
+#       That may not be possible in all circumstances
+#       Also, is may not be desirable. When showing devices, add them at the TOP
+#      f = v.TKFrame
+#      x = f.tk.call('pack', 'info', str(f))
+##      f._saved_w = f._w
+##      f._w = str(f)
+##      logger.debug("repr(f) %s",repr(f))
+##      logger.debug(" str(f) %s", str(f))
+##      x = f.pack_info()
+##      f._saved_w = f._w
+##      v.TKFrame.pack_forget()
+#      logger.debug("Pack_info: %s", x)
+#      
+#    logger.debug("Slave Frames %s", self.TKFrame.pack_slaves())
+
+  def FilterViewDriveRemove(self, FilterKeysAsList):
+    """ Removes view filters from drives 
+      example: FilterViewDriveRemove(self, ['Removable', 'Ejectable']})
+    """
+    for k,v in self.StorageDrives.iteritems():
+      v.FilterViewRemove(FilterKeysAsList)
+
+class TkFDrive(StorageObjects.StorageDrive):
+  """ Tk Frame for Drive """
+  def __init__(self, *args, **kwargs):
+    """
+      Pass argment to StorageObjects.StoragedDrive
+
+      FilterView = {} :
+        Dictionary of Key = Value pairs that make this entry invisible if found inside UDisks2Dict
+    """
+    #logger.debug(self.__class__.__name__)
+    self.ViewFilter = kwargs.pop('FilterView', {})
+    super(self.__class__,self).__init__(*args, **kwargs)
+    self._ChildClass = TkFBlockdevice
+    self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove', background='light blue')
+    self.visible = False    # Not pack()'ed yet
+    self.FilterView() # leads to self.TKFrame.Pack() if visible
+    Tk.Label(self.TKFrame,text="Drive", background='light blue').pack(side=LEFT, anchor="nw")    
+    InfoFrame = Tk.Frame(self.TKFrame, borderwidth=2)
+    Tk.Button(InfoFrame,text="ignore", command = self.OnBtnIgnore, background='light blue').pack(side=RIGHT, anchor="e")
+    if self.UDisks2Dict.get('Ejectable',False):
+      Tk.Button(InfoFrame,text="Eject", command = self.OnBtnEject).pack(side=RIGHT, anchor="e")
+    if self.UDisks2Dict.get('CanPowerOff',False):
+      Tk.Button(InfoFrame,text="PowerOff", command = self.OnBtnPowerOff).pack(side=RIGHT, anchor="e")
+    Tk.Label( InfoFrame,text=self.Id).pack(anchor="nw")
+    # Size as a str ?!?
+    try:
+      Tk.Label( InfoFrame,text=SizeStr(self.Size)).pack(anchor="nw")
+    except ValueError:
+      logger.debug("self.Size: s:%s r:%s",str(self.Size), repr(self.Size))
+    InfoFrame.pack(side=TOP, fill=X, padx=5, pady=5)
+
+  def _close(self):
+    """ A manual destructor"""
+    self.TKFrame.pack_forget()  # Remove visible component
+    self.TKFrame.destroy()      # Kill visible component
+    self.TKFrame = None
+    super(self.__class__,self)._close()
+
+  def OnBtnEject(self):
+    self.Eject()
+ 
+  def OnBtnPowerOff(self):
+    self.PowerOff()
+
+  def OnBtnIgnore(self):
+    #self.Ignore = True   # Make a Filter that always makes thisone ingnore itself.
+    self.FilterViewAdd({'Id':self.UDisks2Dict['Id']})
+    #logger.debug("UDisk2Dict: %s", pformat(self.UDisks2Dict))
+    #self.FilterMakeInvisible()
+
+  def FilterViewAdd(self, MoreFiltersAsDict):
+    """ Adds view filters on this drive 
+      Filtered drives will not show themselves in the main gui.
+      example: FilterViewDriveAdd(self, {'Removable': True})
+      see also: FilterViewRemove, FilterView
+    """
+    self.ViewFilter.update(MoreFiltersAsDict)
+    self.FilterView()
+
+  def FilterViewRemove(self, FilterKeysAsList):
+    """ Removes view filters from this drive
+      example: FilterViewDriveRemove(self, ['Removable', 'Ejectable']})
+      see also: FilterViewAdd, FilterView
+    """
+    for k in FilterKeysAsList:    
+      if k in self.ViewFilter:
+        del self.ViewFilter[k]
+    self.FilterView()
+
+  def FilterView(self):
+    """ Apply Filter against this drive 
+      see also: FilterViewAdd, FilterViewRemove
+    """
+    visible = True
+    for key, test in self.ViewFilter.iteritems():
+      # A little suprising, but self.UDisks2Dict[dbus.String(u'Foo')] is as 
+      # good as self.UDisks2Dict['Foo']
+      #logger.debug("Testing key %s",key)
+      if self.UDisks2Dict.has_key(key):
+        #logger.debug("key found, value is %s", self.UDisks2Dict[key])
+        #logger.debug("Testing %s == %s", test, self.UDisks2Dict[key] == test)
+        if self.UDisks2Dict[key] == test:
+          visible = False
+    if visible:
+      self.FilterMakeVisible()
+    else:
+      self.FilterMakeInvisible()
+
+  def FilterMakeVisible(self):
+    if not self.visible:
+      #logger.debug("")
+      self.TKFrame.pack(side=BOTTOM, fill=X)  # Pack on  Bottom, so that new entries end up on top.
+      self.visible = True
+    
+  def FilterMakeInvisible(self):
+    if self.visible:
+      #logger.debug("")
+      self.TKFrame.pack_forget()
+      self.visible = False
+    
+class TkFBlockdevice(StorageObjects.StorageBlockDevice):
+  """ Tk Frame for Blockdevice """
+  def __init__(self, *args, **kwargs):
+    #logger.debug(self.__class__.__name__)
+    #logger.debug("args %s kwargs %s", args, kwargs)
+    kwargs['ChildClass'] = TkFFilesystem
+    super(self.__class__,self).__init__(*args, **kwargs)
+    self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove', background='light green')
+    self.TKFrame.pack(side=BOTTOM, fill=X)
+    Tk.Label(self.TKFrame,text="BlkDev", background='light green').pack(side=LEFT, anchor="nw")    
+    InfoFrame = Tk.Frame(self.TKFrame, borderwidth=2, relief='flat')
+    #Tk.Label(self.TKFrame,text=self.__class__.__name__).pack(side=TOP, fill=X)    
+    Tk.Button(InfoFrame, text="Write", command=self.TkCmdWrite).pack(side=RIGHT)
+    Tk.Label( InfoFrame, text="{0:s}".format(self.PreferredDevice)).pack(anchor="nw")    
+    Tk.Label( InfoFrame, text="Size "+SizeStr(self.Size)).pack(anchor="nw")    
+    InfoFrame.pack(side=TOP, fill=X, padx=5, pady=5)
+
+  def _close(self):
+    """ A manual destructor"""
+    self.TKFrame.pack_forget()  # Remove visible component
+    self.TKFrame.destroy()      # Kill visible component
+    self.TKFrame = None
+    super(self.__class__,self)._close()
+    
+  def TkCmdWrite(self):
+    logger.debug("Write to %s  -> execute write.sh %s", self.PreferredDevice, self.PreferredDevice)
+    # TODO: This would need a nice subprocess.popen() ... 
+    #os.system("./write.sh %s"%(self.PreferredDevice))
+
+    if True: #not self.proc:
+      cmdarray=" ".join( [
+                      "./write.sh %s"%(self.PreferredDevice),
+                    ] )
+
+      args = shlex.split(cmdarray)
+  
+      self.proc = subprocess.Popen(args,
+                                   #bufsize = 1, # line buffered
+                                   bufsize = 4096, # byte buffered
+                                   stdin = None,
+                                   stdout = None, #subprocess.PIPE,
+                                   stderr = None, #subprocess.STDOUT,
+                                   # preexec_fn unneeded
+                                   close_fds = True, # Do not leak file descriptors
+                                   #cwd = Current Working Directory
+                                   #env Environment mapping
+                                   )
+
+    logger.debug("write.sh executed")
+
+class TkFFilesystem(StorageObjects.StorageFileSystem):
+  """ Tk Frame for Filesystem """
+  def __init__(self, *args, **kwargs):
+    #logger.debug(self.__class__.__name__)
+    super(self.__class__,self).__init__(*args, **kwargs)
+    self._ChildClass = TkFMountpoint
+    self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove')
+    self.TKFrame.pack(side=TOP, fill=X)
+    InfoFrame = Tk.Frame(self.TKFrame, borderwidth=2, relief='flat')
+    #Tk.Label(self.TKFrame,text="Filesystem").pack(anchor="nw")    
+    Tk.Button(InfoFrame,text="Mount", command=self.TkCmdMount).pack(side=RIGHT)
+    Tk.Label( InfoFrame,text=self.IdUsage + " " + self.IdType + " " + self.IdVersion).pack(anchor="nw")    
+    Tk.Label( InfoFrame,text=self.IdUUID + " " + self.IdLabel).pack(anchor="nw")    
+    InfoFrame.pack(side=TOP, fill=X)
+
+  def _close(self):
+    """ A manual destructor"""
+    self.TKFrame.pack_forget()  # Remove visible component
+    self.TKFrame.destroy()      # Kill visible component
+    self.TKFrame = None
+    super(self.__class__,self)._close()
+
+  def TkCmdMount(self, *args, **kwargs):
+    #logger.debug("TkFFilesystem args:%s %s",args,kwargs)
+    self.Mount()
+
+    #logger.debug("TkFFilesystem args:%s %s",args,kwargs)
+
+class TkFMountpoint(StorageObjects.StorageMountPoint):
+  """ Tk Frame for Mountpoint """
+  def __init__(self, *args, **kwargs):
+    #logger.debug("args %s kwargs %s", args, kwargs)
+    #logger.debug(self.__class__.__name__)
+    super(self.__class__,self).__init__(*args, **kwargs)
+    self.TKFrame = Tk.Frame(self.Parent.TKFrame, borderwidth=2, relief='groove', background='#FFC4C4')
+    self.TKFrame.pack(side=TOP, fill=X, ipadx=5, ipady=2)
+    Tk.Button(self.TKFrame,text="Unmount", command=self.TkCmdUnmount).pack(side=RIGHT)
+    Tk.Label(self.TKFrame,text="mounted on " + self.MountPoint).pack(side=LEFT)
+
+#  def __del__(self):
+#    logger.debug("%s.__del__()",self.__class__.__name__)
+#    super(self.__class__,self).__del__()
+
+  def _close(self):
+    """ A 'manual' destructor"""
+    #logger.debug("%s _close for %s",self.__class__.__name__,str(self))
+    self.TKFrame.pack_forget()  # Remove visible component
+    self.TKFrame.destroy()      # Kill visible component
+    self.TKFrame = None
+    super(self.__class__,self)._close()
+    
+  def TkCmdUnmount(self, *args, **kwargs):
+    #logger.debug("TkFMountpoint args:%s %s",args,kwargs)
+    self.Unmount()
+  
+  
+#end;
\ No newline at end of file
diff --git a/MassStorageCloner/blkwrite.sh b/MassStorageCloner/blkwrite.sh
new file mode 100755
index 0000000..4003234
--- /dev/null
+++ b/MassStorageCloner/blkwrite.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+#
+# Mass Storage Cloner - blkwrite.sh
+# (C)2015 Martin Suefke
+# License: GPLv3 or newer
+#
+# writes source file $1 to block device $2
+#
+# DO NOT Customize this file. Try to customize write.sh first
+#
+
+set -e      # break if error
+
+function onerror() {
+    echo
+    echo "Trapped error while copying >$SRC< to >$TGT< Result code is \"$res\"".
+    echo -n "Press Enter to Exit this script ..."
+    read -n1
+    trap EXIT
+    exit 1
+}
+
+trap onerror ERR EXIT
+
+echo "Writing >$1< to >$2<"
+if [ $# -lt 2 ]
+then
+  echo "Need 2 parameters: source target"
+  exit 1
+fi
+
+SRC="$1"
+TGT="$2"
+
+if [ ! -b "$TGT" ]
+then
+    echo "need a blockdevice as parameter 2"
+    exit 2
+fi
+
+if [ "x${TGT%%[0-9]*}" == "x$TGT" ]
+then
+  # have a "master" block device
+  echo "removing partitions from $TGT with partx"
+  partx -d "$TGT" || true
+fi
+
+echo "Start copy operation"
+# live version
+pv -trabe -B8m $SRC | dd bs=8M iflag=fullblock of="$TGT" oflag=sync
+# debug version
+#pv -trabe -B8m $SRC | dd bs=8M iflag=fullblock of=/dev/null oflag=sync
+res="$?"
+echo "Result code $res"
+
+if [ "x${TGT%%[0-9]*}" == "x$TGT" ]
+then
+  # have a "master" block device
+  echo "adding partitions for $TGT with partx"
+  partx -a "$TGT"
+fi
+
+if [ x$res == x0 ]
+then
+    trap EXIT
+else
+    onerror
+fi
+#end;
diff --git a/MassStorageCloner/dbusdetect.py b/MassStorageCloner/dbusdetect.py
new file mode 100644
index 0000000..d4a2cbb
--- /dev/null
+++ b/MassStorageCloner/dbusdetect.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+
+"""
+ DBus Removable Mass Storage Detection
+ (C) 2014 Martin Süfke
+ License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html
+ partly based on http://stackoverflow.com/questions/23244245/listing-details-of-usb-drives-using-python-and-udisk2
+"""
+
+import logging
+
+if len(logging.root.handlers) == 0:
+# SPE compatible logging format
+  logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d')
+logger = logging.getLogger(__name__)
+
+from pprint import pformat#, pprint
+#from StorageObjects import Storage
+
+import dbus
+from dbus.mainloop.glib import DBusGMainLoop
+dbus.mainloop.glib.threads_init()
+DBusGMainLoop(set_as_default=True)
+#dbus.set_default_main_loop()
+bus = dbus.SystemBus()
+ud_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2')
+om = dbus.Interface(ud_manager_obj, 'org.freedesktop.DBus.ObjectManager')
+
+def GetDriveId(DriveDict):
+  """ Input a Drive dict (from key = value) returned from a GetManagedObjects() call
+      Output: UDisks2.Drive.Id
+  """
+  try:
+    return DriveDict[
+      dbus.String(u'org.freedesktop.UDisks2.Drive')][
+      dbus.String(u'Id')
+      ]
+  except: 
+    return "no [org.freedesktop.UDisks2.Drive][Id] in "+pformat(DriveDict)
+
+
+def GetDrive(BlockDict):
+  """ Input a BlockDev dict (from key = value) returned from a GetManagedObjects() call
+      Output: UDisks2.Drive dict
+  """
+  try:
+    return BlockDict[dbus.String(u'org.freedesktop.UDisks2.Block')][dbus.String(u'Drive')]
+  except: 
+    return "no [org.freedesktop.UDisks2.Block][Drive] in "+pformat(BlockDict)
+
+def ScanDevices(AllStorage = None, RequireRemovable = False):
+  global om
+  udisks_path_drives={}
+  udisks_path_drives_removable={}
+  udisks_path_blockdev={}
+  
+  ManagedObjects=om.GetManagedObjects()
+  for k,v in ManagedObjects.iteritems():
+    #print ""
+    #print "Key:",k
+    d=dict(v) 
+    #print "dict:", isinstance(v, dict)
+    udisks_is_blockdev = v.has_key(dbus.String(u'org.freedesktop.UDisks2.Block'))
+    udisks_is_drive = v.has_key(dbus.String(u'org.freedesktop.UDisks2.Drive'))
+    #print "Block?", udisks_is_blockdev
+    if udisks_is_blockdev:
+      udisks_path_blockdev[k]=v
+      #pprint(d)
+    elif udisks_is_drive:
+      udisks_path_drives[k]=v
+      if RequireRemovable == False or v.\
+        get(dbus.String(u'org.freedesktop.UDisks2.Drive'), False).\
+        get(dbus.String(u'Removable'),False):
+        udisks_path_drives_removable[k]=v
+      #pprint(d)
+  
+  # Removing all blockdevs that have no "link" to any of the drives selected
+  udisks_path_blockdev_removable={}
+  for b,d in udisks_path_blockdev.iteritems():
+    blockdrive = d[dbus.String(u'org.freedesktop.UDisks2.Block')][dbus.String(u'Drive')]
+    d['U2.Block.Drive'] = blockdrive
+    if blockdrive in udisks_path_drives_removable:
+      # See if there is filesystem(s) on it
+      filesystem = d.get(dbus.String(u'org.freedesktop.UDisks2.Filesystem'))
+      d['U2.Filesystem'] = filesystem
+      blockdevices =  udisks_path_drives_removable[blockdrive].get('U2.BlockDevices', dict())
+      blockdevices[b] = d
+      udisks_path_drives_removable[blockdrive]['U2.BlockDevices'] = blockdevices
+      udisks_path_blockdev_removable[b] = d
+  
+  if AllStorage is not None:
+    for k,d in udisks_path_drives_removable.iteritems():
+      AllStorage.AddDrive(k,d.get(dbus.String(u'org.freedesktop.UDisks2.Drive')), EjectByUDisks, PowerOffByUDisks)
+    
+    for k,d in udisks_path_blockdev_removable.iteritems():
+      AllStorage.AddBlockDevice(k,d.get(dbus.String(u'org.freedesktop.UDisks2.Block')))
+      FsDict = d.get(dbus.String(u'org.freedesktop.UDisks2.Filesystem'))
+      if FsDict is not None:
+        AllStorage.AddFileSystem(k,FsDict,MountByUDisks,UnmountByUDisks)
+
+  # Output the scan results for the log
+  # TODO: (re)move this section; output from storage AllStore
+  logger.debug("""
+    - ScanRemovableDevices() Results -
+    """
+  )
+  for k,d in udisks_path_drives_removable.iteritems():
+    logger.debug("Drive: %s", GetDriveId(d))
+    #, "removable:", udisks_path_drives[d].\
+    #  get(dbus.String(u'org.freedesktop.UDisks2.Drive'), False).\
+    #  get(dbus.String(u'Removable'),False)
+  #  print
+    
+  for k,d in udisks_path_blockdev_removable.iteritems():
+    logger.debug("Blockdev: %s %s", k.rsplit('/',1)[1], GetDriveId(udisks_path_drives_removable[GetDrive(d)])) 
+      
+  logger.debug("""
+    - ScanRemovableDevices() Results End-
+    """
+  )
+
+def EjectByUDisks(UDisk2PathToDrive):
+  """ Call with a UDisks2 path to a drive """
+  global bus
+  logger.debug("Eject %s", UDisk2PathToDrive)
+  dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToDrive)
+  fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Drive')
+  logger.info(fs_intf.Eject({}))
+
+def PowerOffByUDisks(UDisk2PathToDrive):
+  """ Call with a UDisks2 path to a drive """
+  global bus
+  logger.debug("PowerOff %s", UDisk2PathToDrive)
+  dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToDrive)
+  fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Drive')
+  logger.info(fs_intf.PowerOff({}))
+
+def MountByUDisks(UDisk2PathToBlockDevice):
+  """ Call with a UDisks2 path to a block device """
+  global bus
+  logger.debug("Mount %s", UDisk2PathToBlockDevice)
+  dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToBlockDevice)
+  fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Filesystem')
+  logger.info(fs_intf.Mount({}))
+
+def UnmountByUDisks(UDisk2PathToBlockDevice):
+  """ Call with a UDisks2 path to a block device """
+  global bus
+  logger.debug("Unmount %s", UDisk2PathToBlockDevice)
+  dbus_obj = bus.get_object('org.freedesktop.UDisks2', UDisk2PathToBlockDevice)
+  fs_intf = dbus.Interface(dbus_obj, 'org.freedesktop.UDisks2.Filesystem')
+  logger.info(fs_intf.Unmount({}))
+
+#def UDisks2Signal_SignalReceived(*args, **kwargs):
+#  print "UDisks2 Signal Received: {0!s} {1!s}\n".format(args,kwargs)
+
+def InstallChangeReceiver(
+      PropertiesChangedHandler=None,
+      InterfaceAddedHander=None,
+      InterfaceRemovedHandler=None,
+      ):
+        
+  logger.debug(
+    """Object Manager
+  Path: %s 
+  Intf: %s """,
+    om.object_path, 
+    om.dbus_interface
+    )
+#  bus.add_signal_receiver(handler_function=UDisks2Signal_SignalReceived, 
+#                          interface_keyword='interface',
+#                          member_keyword='member',
+#                          sender_keyword='sender',
+#                          destination_keyword='destination',
+#                          path_keyword='path',
+#                          message_keyword='message',
+#                        )
+  if callable(PropertiesChangedHandler):
+    bus.add_signal_receiver(handler_function=PropertiesChangedHandler,
+                          dbus_interface=dbus.String(u'org.freedesktop.DBus.Properties'),
+                          path_keyword='path',
+                          byte_arrays=True,
+                          )
+
+  if callable(InterfaceAddedHander):
+    om.connect_to_signal(signal_name="InterfacesAdded", 
+                       handler_function=InterfaceAddedHander,
+#                       path_keyword='path',
+                       byte_arrays=True,
+                       )
+
+  if callable(InterfaceRemovedHandler):
+    om.connect_to_signal(signal_name="InterfacesRemoved", 
+                       handler_function=InterfaceRemovedHandler,
+ #                      path_keyword='path',
+                       byte_arrays=True,
+                       )
+                       
+  logger.debug("succcess")
+
+if __name__ == "__main__":
+#  logging.basicConfig(level=logging.WARN)
+#  raise Exception("{0} cannot run as the main python program".format(__file__))
+  logger.warn("Running {0} as a program. Will scan for devices and exit.")
+  ScanDevices()
+
+#end;
\ No newline at end of file
diff --git a/MassStorageCloner/gui.py b/MassStorageCloner/gui.py
new file mode 100755
index 0000000..132e4e2
--- /dev/null
+++ b/MassStorageCloner/gui.py
@@ -0,0 +1,391 @@
+#! /usr/bin/python
+# -*- coding: UTF-8 -*-
+
+"""
+ Mass Storage Cloner
+ writes an image file to many mass storage devices with a gui
+ (C) 2014 Martin Süfke
+ License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html
+"""
+
+"""
+ History: 
+
+2014-10-14 started
+2015-03-26 added write.sh quick hack for LIP SS2015, released on redmine.fsmpi.rwth-aachen.de
+"""
+
+# Proper logging
+from pprint import pprint, pformat
+import logging
+# SPE compatible logging format
+logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d')
+logger = logging.getLogger(__name__)
+
+#import stacktrace
+import inspect
+
+#import  Tkinter #as Tk
+try:
+  import  ttk as Tk
+#  Tk.Tk = Tkinter.Tk # need a Tk.Tk to make the root Window. Faking one.
+except:
+  logger.info("Tiling Toolkit (ttk) not found. Falling back to classic Tk")
+  import  Tkinter as Tk
+
+#from    Tkinter import BooleanVar, IntVar, DoubleVar, StringVar
+#import  tkMessageBox as dialog
+import  tkFileDialog as filedialog
+from    Tkconstants import N,E,S,W,HORIZONTAL,TOP,BOTTOM,BOTH, NORMAL, DISABLED
+from    vtk import VCheckbutton, VSLabel, VIScale, Vset, VGridFrame#, Vget
+import  dbus
+
+import dbusdetect
+import gobject
+
+import StoreObjFrames 
+
+import os.path
+import sys
+
+import threading
+import Queue
+
+LastSavedFile=None
+
+def DoLoadFile():
+  filename = filedialog.askopenfilename(
+    filetypes=[ ("Image files", "*.img"),
+# TODO: zipped/bzipped/xzipped images ?               
+#               ("All files", "*.*"),
+                ("All files", "*"),
+              ],
+    title="Select image file to write"
+  )
+  logger.debug( "Open dialog returns >>>%s<<< ",str(filename))
+  if (len(filename)==0) or (str(filename).strip("\0\t\r\n ")==""):
+    return
+  
+def Implement(msg=""):
+  logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4])
+
+class ThreadEventedQueue(Queue.Queue):
+  def __init__(self, EventName, TkRoot=None, maxsize=0):
+    # Cannot get super() to work ?!?
+    #super(ThreadEventedQueue, self ).__init__(maxsize)   
+    Queue.Queue.__init__(self, maxsize)
+    self.EventName = '<<{0:s}>>'.format(EventName)
+    self.TkRoot = TkRoot
+
+  def AddEvent(self, EventObj):
+    self.put(EventObj)
+    if self.TkRoot is not None:
+      self.TkRoot.event_generate(self.EventName, when='tail')
+  
+  def SetRoot(self, TkRoot, Callback):
+    self.TkRoot = TkRoot
+    self.TkRoot.bind(self.EventName, Callback)
+  
+class MSClonerGui(threading.Thread):
+  """
+  Separate Thread according to 
+  http://stackoverflow.com/questions/459083/how-do-you-run-your-own-code-alongside-tkinters-event-loop
+  """
+  def __init__(self, autostart=True, NotifierThread = None):
+    threading.Thread.__init__(self)
+    self.NotifierThread = NotifierThread
+    self.ThreadNotifyQueue = ThreadEventedQueue('MyUDisk2Notify', None)
+    #self.DoQuit = False
+    if autostart:
+      self.start()
+    
+  def BuildGui(self):
+    try:  
+      self.root = Tk.Tk()
+    except AttributeError:
+      self.root = Tk.Tkinter.Tk()
+  
+    #build the GUI on 'root'
+    self.root.wm_title("MassStorageCloner - GPLv3 or newer (C) 2014 Martin Süfke")
+    FrameHead = VGridFrame(self.root, relief='raised')
+    self.CbRemovableOnly = VCheckbutton(master=FrameHead, text="Removable only", command=self.OnCbRemovableOnly)
+    FrameContent = Tk.Frame(self.root)
+    FrameContDevice = Tk.Frame(FrameContent)
+      
+    self.root.columnconfigure(0,weight=1)
+    self.root.rowconfigure(0,weight=1)
+    FrameHead.grid(column=0, row=0, sticky=(N,E,W))
+    FrameHead.grid_next([self.CbRemovableOnly,
+                         Tk.Button(master=FrameHead, text="Unhide ignored", command=self.OnUnhideIgnored),
+                         ])    
+    FrameContent.grid(column=0, row=1, sticky=(N,S,E,W))
+    FrameContent.columnconfigure(0,weight=1)
+    FrameContent.rowconfigure(0,weight=1)
+    FrameContDevice.grid(sticky=(N,E,S,W))
+  
+    self.TKStorageFrame = StoreObjFrames.TkFStorage(FrameContDevice)
+    dbusdetect.ScanDevices(self.TKStorageFrame)
+
+  def run(self):
+    logger.debug("GUI thread run()")
+      #self.root.update()
+    self.BuildGui()
+    logger.debug("GUI thread built GUI")
+    # Activate Event system
+    self.ThreadNotifyQueue.SetRoot(self.root, self.ThreadNotifyCallback)
+    #if not self.DoQuit:
+    self.root.mainloop()
+    logger.debug("Tk mainloop() ended")
+    if self.NotifierThread is not None:
+      if self.NotifierThread.is_alive():
+        self.NotifierThread.quit()
+      else:
+        logger.debug('NotifierThread already dead')
+        
+  def quit(self):
+    #self.DoQuit = False
+    #try:
+    self.root.quit()
+    #except:
+    #  pass
+
+  def OnCbRemovableOnly(self):
+    """ CbRemovableOnly has been checked or unchecked """
+    #logger.debug("CbRemovableOnly(): %s", self.CbRemovableOnly.value )
+    if self.CbRemovableOnly.value:
+      self.TKStorageFrame.FilterViewDriveAdd({'Removable':False})
+    else:
+      self.TKStorageFrame.FilterViewDriveRemove(['Removable', ])
+
+  def OnUnhideIgnored(self):
+    """ Unhide Ignore has been clicked"""
+    self.TKStorageFrame.FilterViewDriveRemove(['Id', ])
+
+  def SignalDBusEvent(self, EventObj):
+    """ This can be called by the DBus notifier thread """
+    assert self.ThreadNotifyQueue is not None
+    self.ThreadNotifyQueue.AddEvent(EventObj)
+    
+  def ThreadNotifyCallback(self, event):
+    try:
+      while True:
+        ev = self.ThreadNotifyQueue.get_nowait()
+        logger.debug("GUI Thread %s %s", ev, event)
+        self.ProcessNotifyEvent(ev)
+        self.ThreadNotifyQueue.task_done()
+    except Queue.Empty:
+      pass
+
+  def ProcessNotifyEvent(self, event):
+    """ event is a tuple containing at least an Identifier in event[0] 
+      called by ThreadNotifyCallback()
+    """
+    assert len(event)>0, "Event tuple length >0 required, got {0!s}".format(event)
+    if event[0] == 'MountPointsChanged':
+      #assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
+      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
+      self.TKStorageFrame.UpdateMountPoints(event[1], event[2], dbusdetect.UnmountByUDisks)
+    elif event[0] == 'DriveAdded':
+      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
+      self.TKStorageFrame.AddDrive(event[1], event[2], dbusdetect.EjectByUDisks, dbusdetect.PowerOffByUDisks)
+    elif event[0] == 'BlockDeviceAdded':
+      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
+      self.TKStorageFrame.AddBlockDevice(event[1], event[2])
+    elif event[0] == 'FileSystemAdded':
+      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
+      self.TKStorageFrame.AddFileSystem(event[1],event[2], dbusdetect.MountByUDisks, dbusdetect.UnmountByUDisks)
+    elif event[0] == 'DriveRemoved':
+      #self.TKStorageFrame.AddDrive(event[1], event[2], dbusdetect.EjectByUDisks, dbusdetect.PowerOffByUDisks)
+      assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event)
+      self.TKStorageFrame.RemoveDrive(event[1])
+    elif event[0] == 'BlockDeviceRemoved':
+      #self.TKStorageFrame.AddBlockDevice(event[1], event[2])
+      assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event)
+      self.TKStorageFrame.RemoveBlockDevice(event[1])
+    elif event[0] == 'FileSystemRemoved':
+      assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event)
+      self.TKStorageFrame.RemoveFileSystem(event[1])
+      #self.TKStorageFrame.AddFileSystem(event[1],event[2], dbusdetect.MountByUDisks, dbusdetect.UnmountByUDisks)
+    else:
+      logger.warn("Unknown Event %s",event[0])
+
+class MSGObjectNotifier(threading.Thread):
+  """ Another thread to contain the GObject mainloop """
+  def __init__(self, GuiThread = None):
+    threading.Thread.__init__(self)
+    self.GuiThread = GuiThread
+
+  def run(self):
+    logger.debug("GObject thread started")
+    try:
+      gobject.threads_init() # the key to make gobject threads run in python
+      self.loop = gobject.MainLoop()
+      self.context = self.loop.get_context()
+      self.InstallDBusListener()
+      logger.debug("running GObject mainloop()")
+      self.loop.run()
+    finally:
+      logger.debug("GObject mainloop() ended")
+      if self.GuiThread is not None:
+        if self.GuiThread.is_alive():
+          self.GuiThread.quit()
+        else:
+          logger.debug('GuiThread already dead')
+    
+  def quit(self):
+    self.loop.quit()
+
+  def InstallDBusListener(self):
+    dbusdetect.InstallChangeReceiver(
+      PropertiesChangedHandler=self.UDisks2Signal_PropertiesChanged,
+      InterfaceAddedHander=self.UDisks2Signal_InterfaceAdded,
+      InterfaceRemovedHandler=self.UDisks2Signal_InterfaceRemoved,
+    )
+
+  def UDisks2Signal_PropertiesChanged(self, *args, **kwargs):
+    """ Callback for any DBus.Properties -> PropertiesChanged """
+  #  print "UDisks2 Properties Changed: {0!s} {1!s}\n".format(args,pformat(kwargs))
+    if (len(args) >= 3) and (len(kwargs) >= 1):
+      # May have args: subject, message, signature   (TODO: How are these defined in DBus ?)
+      if 'org.freedesktop.UDisks2.Filesystem' == args[0]:
+        if isinstance(args[1],dict) and ('MountPoints' in args[1]):
+          # don't care about args[2] / Signature
+          U2MountPoints=args[1].get('MountPoints',[])
+          #logger.debug('U2Path: %s %s',type(kwargs), pformat(kwargs))
+          U2Path=kwargs.get('path','')
+          #logger.debug('U2Path: %s',U2Path)
+          if U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'):
+            # Have a change in Block Devices
+            self.SignalGuiMountPointChanged(U2Path, U2MountPoints)
+            return
+            
+    assert logger is not None
+    logger.info("Unhandled Message %s %s\n", args, pformat(kwargs))
+      
+  def UDisks2Signal_InterfaceAdded(self, *args, **kwargs):
+    """ Callback for any UDisks2.ObjectManager -> InterfaceAdded 
+    Through this call go drives and blockdevices that are added """
+    if (len(args) >= 2):
+      U2Path = args[0]
+      if isinstance(args[1], dict):
+        U2Dict = args[1]
+        #logger.debug("U2Dict: %s\n", pformat(U2Dict))
+      else:
+        U2Dict = {}
+      if U2Path.startswith('/org/freedesktop/UDisks2/drives/'):
+        #logger.debug("Drive added %s\n", args[0].rsplit('/')[-1])
+        DriveDict = U2Dict.get('org.freedesktop.UDisks2.Drive')
+        if DriveDict is not None:
+          self.SignalGuiDriveAdded(U2Path, DriveDict)
+          return
+      elif U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'):
+        #logger.debug("Blockdevice added %s\n", args[0].rsplit('/')[-1])
+        BlockDict = U2Dict.get('org.freedesktop.UDisks2.Block')
+        if BlockDict is not None:
+          self.SignalGuiBlockDeviceAdded(U2Path, BlockDict)
+        FsDict = U2Dict.get('org.freedesktop.UDisks2.Filesystem')
+        if FsDict is not None:
+          self.SignalGuiFileSystemAdded(U2Path, FsDict)
+        if (BlockDict is not None) or (FsDict is not None):
+          return
+        
+    assert logger is not None
+    logger.info("Unknown %s %s\n", args, pformat(kwargs))
+  
+  def UDisks2Signal_InterfaceRemoved(self, *args, **kwargs):
+    """ Callback for any UDisks2.ObjectManager -> InterfaceRemoved """
+    if (len(args) >= 2):
+      U2Path = args[0]
+      if isinstance(args[1], list):
+        U2List = args[1]
+      else:
+        U2List = []
+      Unknown = True
+      if U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'):
+        if 'org.freedesktop.UDisks2.Filesystem' in U2List:
+          self.SignalGuiFileSystemRemoved(U2Path)
+          Unknown = False
+        if 'org.freedesktop.UDisks2.Block' in U2List:
+          self.SignalGuiBlockDeviceRemoved(U2Path)
+          Unknown = False
+      if U2Path.startswith('/org/freedesktop/UDisks2/drives/'):
+        #logger.debug("Drive removed %s\n", args[0].rsplit('/')[-1])
+        if 'org.freedesktop.UDisks2.Drive' in U2List:
+          self.SignalGuiDriveRemoved(U2Path)
+          Unknown = False
+
+    if not Unknown:
+      return
+
+    assert logger is not None
+    logger.info("Unknown %s %s\n", args, pformat(kwargs))
+
+  def SignalGuiMountPointChanged(self, U2Path, U2MountPoints):
+    logger.debug("GObj() path %s mp %s\n", U2Path, U2MountPoints)
+    EvObj=('MountPointsChanged', U2Path, U2MountPoints)
+    self.GuiThread.SignalDBusEvent(EvObj)
+
+  def SignalGuiDriveAdded(self, U2Path, U2Dict):
+    logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys())
+    EvObj=('DriveAdded', U2Path, U2Dict)
+    self.GuiThread.SignalDBusEvent(EvObj)
+
+  def SignalGuiBlockDeviceAdded(self, U2Path, U2Dict):
+    logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys())
+    EvObj=('BlockDeviceAdded', U2Path, U2Dict)
+    self.GuiThread.SignalDBusEvent(EvObj)
+
+  def SignalGuiFileSystemAdded(self, U2Path, U2Dict):
+    logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys())
+    EvObj=('FileSystemAdded', U2Path, U2Dict)
+    self.GuiThread.SignalDBusEvent(EvObj)
+
+  def SignalGuiEventRemoved(self, name, param1):
+    EvObj=(name, param1)
+    self.GuiThread.SignalDBusEvent(EvObj)
+
+  def SignalGuiDriveRemoved(self, U2Path):
+    self.SignalGuiEventRemoved('DriveRemoved', U2Path)
+
+  def SignalGuiBlockDeviceRemoved(self, U2Path):
+    self.SignalGuiEventRemoved('BlockDeviceRemoved', U2Path)
+
+  def SignalGuiFileSystemRemoved(self, U2Path):
+    self.SignalGuiEventRemoved('FileSystemRemoved', U2Path)
+    
+gnotifier =  MSGObjectNotifier()
+gui = MSClonerGui(NotifierThread=gnotifier)
+gnotifier.GuiThread = gui
+gnotifier.start()
+
+#dbusdetect.InstallChangeReceiver()
+
+#logger.debug('Time to do more in the main thread')
+# Do not know what to do here (sensibly) to wait ... trying join()
+#import time
+#while gui.is_alive() and gnotifier.is_alive():
+#    time.sleep(1)
+#    logger.debug('tick ')
+
+# Waiting for a key press is not smart either
+#logger.info("Waiting for key press on stdin")
+#sys.stdin.read(1)
+#gnotifier.quit()
+#gui.quit()
+
+logger.debug('Waiting for thread joins')
+gnotifier.join()
+gui.join()     # don't know: join() again or not ?
+logger.debug('End, threads joined')
+
+"""make use of figure.ginput() for mouse input or waitforbuttonpress() for keyboard-stuff
+"""
+
+def _quit(self):
+  logger.debug("called python _quit()")
+  if gui is not None:
+    gui.quit()     # stops mainloop
+    gui.root.destroy()  # this is necessary on Windows to prevent
+                        # Fatal Python Error: PyEval_RestoreThread: NULL tstate
+
+#end;
diff --git a/MassStorageCloner/vtk.py b/MassStorageCloner/vtk.py
new file mode 100644
index 0000000..2a365b3
--- /dev/null
+++ b/MassStorageCloner/vtk.py
@@ -0,0 +1,223 @@
+#! /usr/bin/python
+# -*- coding: UTF-8 -*-
+"""
+ Visual with Variable TK components.
+ These elements extend the widgets in Tkinter to include their variables
+ in one pythonic object.
+
+ (C) 2012-2014 Martin Süfke
+ License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html
+"""
+try:
+  from ttk import BooleanVar, IntVar, DoubleVar, StringVar
+  from ttk import Checkbutton, Label, Scale, Entry, OptionMenu, Text, Frame
+except:
+  from Tkinter import BooleanVar, IntVar, DoubleVar, StringVar
+  from Tkinter import Checkbutton, Label, Scale, Entry, OptionMenu, Text, Frame
+#from Tkconstants import *
+import types
+import io
+
+def _setifunset(d,n,v):
+  if not n in d:
+    d[n]=v
+
+class Vget():
+  @property
+  def value(self):
+    return self.variable.get()
+  def get(self):
+    return self.variable.get()
+
+class Vset():
+  #TODO: make a setter property on ""value" ?
+  def set(self,val):
+    self.variable.set(val)
+
+class Vvalue(Vget):
+  @property
+  def value(self):
+    return self.variable.get()
+  @value.setter
+  def value_setter(self,newvalue):
+    return self.variable.set(newvalue)
+
+
+class VsetFmt(Vset):
+  def set(self,msg,*arg):
+    #msg = msg
+    #if len(arg)>0:
+    #  self.variable.set(msg % arg)
+    #else:
+    Vset.set(self,msg%arg)
+
+class VCheckbutton(Checkbutton,Vget,Vset,Vvalue,Vvalue):
+  """Checkbutton with Boolean value"""
+  def __init__(self, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    self.variable=BooleanVar(value=kw.pop("value",False))
+    _setifunset(kw,"offvalue",False)
+    _setifunset(kw,"onvalue",True)
+    _setifunset(kw,"variable",self.variable)
+    Checkbutton.__init__(self, *arg, **kw)
+
+class VDLabel(Label,Vget,Vset,Vvalue):
+  """Label with DoubleVar() Value"""
+  def __init__(self, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    self.variable=DoubleVar(value=kw.pop("value",0.0))
+    _setifunset(kw,"textvariable",self.variable)
+    Label.__init__(self, *arg, **kw)
+
+class VSLabel(Label,Vget,VsetFmt):
+  """Label with StringVar() Value"""
+  def __init__(self, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    self.variable=StringVar(value=kw.pop("value",""))
+    _setifunset(kw,"textvariable",self.variable)
+    Label.__init__(self, *arg, **kw)
+
+class VIScale(Scale,Vget,Vset,Vvalue):
+  """Scale with IntVar() Value"""
+  def __init__(self, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    self.variable=IntVar(value=kw.pop("value",0))
+    _setifunset(kw,"variable",self.variable)
+    Scale.__init__(self, *arg, **kw)
+
+class VSEntry(Entry,Vget,VsetFmt):
+  """Entry with StringVar() Value"""
+  def __init__(self, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    self.variable=StringVar(value=kw.pop("value",0))
+    _setifunset(kw,"textvariable",self.variable)
+    Entry.__init__(self, *arg, **kw)
+
+class VIOptionMenu(OptionMenu,Vget,Vset,Vvalue):
+  """OptionMenu with IntVar() Value"""
+  def __init__(self, master, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    value=kw.pop("value",0)
+    self.variable=IntVar(value=value)
+    var=kw.pop("variable",self.variable)
+    OptionMenu.__init__(self, master, var, value, *arg, **kw) # this is a mess in TKinter
+
+class VSOptionMenu(OptionMenu,Vget,Vset,Vvalue):
+  """OptionMenu with StringVar() Value"""
+  def __init__(self, master, *arg, **kw):
+    # this is an inherited __init__ using "classic" style without super()
+    value=kw.pop("value",0)
+    self.variable=StringVar(value=value)
+    var=kw.pop("variable",self.variable)
+    OptionMenu.__init__(self, master, var, value, *arg, **kw) # this is a mess in TKinter
+
+class VText(Text):
+  """A Text widget with a more spohisticated interface"""
+  def __init__(self, *arg, **kw):
+    Text.__init__(self, *arg, **kw)
+
+  def FromList(self,list):
+    """Put all text from list into Text Widget"""
+    self.delete('1.0','end')
+    for line in list:
+      self.insert('end',line+"\n")
+
+  def FromString(self,input):
+    """convert input string with line breaks (cr, lf, cr/lf) into Text Widget"""
+    sio=io.StringIO(unicode(input))
+    self.delete('1.0','end')
+    line=sio.readline()
+    while ""<line:
+      self.insert('end',line)
+      line=sio.readline()
+    sio.close()
+
+class VGridFrame(Frame):
+  """A Frame widget with a more spohisticated grid layout interface"""
+
+  nextCol="col"
+  nextRow="row"
+
+  def __init__(self, *arg, **kw):
+    self.set_next(kw.pop('next', self.nextCol)) # put in colum or row
+    Frame.__init__(self, *arg, **kw)
+    self._col=0  # next position to grid into
+    self._row=0
+    self._sticky=None # no stickyness
+
+  def colconfig(self,**kw):
+    """Apply columnconfigure() to the current column.
+    Usually used before grid_next()"""
+    self.columnconfigure(self._col,**kw)
+
+  def rowconfig(self,**kw):
+    """Apply rowconfigure() to the current row.
+    Usually used before grid_next()"""
+    self.rowconfigure(self._row,**kw)
+
+  def set_next(self,next):
+    """set next step direction to 'col' or 'row'"""
+    if next:
+      if next == self.nextCol:
+        self._next=self.nextCol
+      elif next == self.nextRow:
+        self._next=self.nextRow
+      else:
+        assert False, "Need next='%s' or next='%s' but got %s"%(self.nextCol,self.nextRow,next)
+
+  def grid_next(self,widget_or_list,**kw):
+    """apply widget.grid() with increasing colum= or row= values
+    grid_next( (A, B, ..), opt) is a shortcurt for
+    grid_next( A, opt)
+    grid_next( B, opt)
+    grid_next( .., opt)
+    """
+
+    def _grid(widget):
+      widget.grid(column=self._col, row=self._row, **kw)
+      if self._next == self.nextCol:
+        self._col+=colinc
+      elif self._next == self.nextRow:
+        self._row+=rowinc
+
+    self._col=kw.pop('column', self._col)
+    self._row=kw.pop('row', self._row)
+    colinc=kw.get('columnspan',1)
+    rowinc=kw.get('rowspan',1)
+
+    self.set_next(kw.pop('next',None))
+    try:
+      sticky=kw.pop('sticky')
+      self._sticky=sticky # save stickyness for next time
+    except KeyError:
+      sticky=self._sticky # get last value (may be None if sticky unset)
+    if sticky:
+      kw['sticky']=sticky
+
+    if isinstance( widget_or_list, (types.TupleType,types.ListType) ):
+      for w in widget_or_list:
+        _grid(w)
+    else:
+      _grid(widget_or_list) # is a widget
+
+  def step(self,**kw):
+    """ Step a column or a row forward, resetting column or row
+    usage:
+      step(column=0) resets to column 0, advancing row
+      step(row=2) resets to row 2, advancing column
+      step(column=3, row=4) sets to coulmn 3 and row 4"""
+    col=kw.get("column",None)
+    row=kw.get("row",None)
+    if col is not None:
+      self._col=col
+      self._row=row or self._row+1
+    elif row is not None:
+      self._row=row
+      self._col=col or self._col+1
+    else:
+      assert False, "You need either 'column=...' or 'row=...' in step() %r"%(kw)
+
+## check not running as main
+assert '__main__' != __name__ , "You should not run this, but [import] it"
+
+#end;
diff --git a/MassStorageCloner/write.sh b/MassStorageCloner/write.sh
new file mode 100755
index 0000000..5d078d4
--- /dev/null
+++ b/MassStorageCloner/write.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+#
+# Mass Storage Cloner - write.sh
+# (C)2015 Martin Suefke
+# License: GPLv3 or newer
+#
+# This is exected whenever the user clicks "write" on any blockdev
+# The blockdev is passed as $1
+#
+# Customize this file as needed.
+#
+
+xterm -e "sudo bash ./blkwrite.sh ~/LIP-Image-WS2014-10-09-01.img $1"
+
+#end;
-- 
GitLab