Permalink
Browse files

2010-05-14 Niall Power - <niall.power@oracle.com>

        * etc/dbus-1/system.d/time-slider.conf:
          Add service and policy definitions for TimSlider.config
          service.

        * usr/share/time-slider/lib/plugin/rsync/backup.py:
          Deal with backup device being mounted under variable
          removable media mount points if not found in it's expected
          location. Find and validate alternate mount points.

        * usr/share/time-slider/lib/time_slider/applet.py:
          Refactored to allow more modular notification management.
          Improved rsync device monitoring by implementing both gio.File
          and gio.Volume monitors so we can track mounting/unmounting of
          static mounts like nfs/zfs and variable mount points for
          hotpluggable devices. Validate rsync target is correct using
          SMF configuration key. Listen to TimeSlider.Config D-Bus events
          and refresh configuration when D-Bus notification is received.
          Add menu item to enable launching of setup GUI from the applet.

        * usr/share/time-slider/lib/time_slider/dbussvc.py:
          Add class and method definition for TimeSlider Config service
          notifications.

        * usr/share/time-slider/lib/time_slider/setupgui.py:
          Register with system D-Bus and send notification of changes
          to time-slider and rsync plugin services so the applet can
          refresh.
  • Loading branch information...
1 parent fa14b28 commit d9df954e378de80554c1e74e068fbbe91966d69f @NiallPower NiallPower committed May 13, 2010
View
30 ChangeLog
@@ -1,3 +1,33 @@
+2010-05-14 Niall Power - <niall.power@oracle.com>
+
+ * etc/dbus-1/system.d/time-slider.conf:
+ Add service and policy definitions for TimSlider.config
+ service.
+
+ * usr/share/time-slider/lib/plugin/rsync/backup.py:
+ Deal with backup device being mounted under variable
+ removable media mount points if not found in it's expected
+ location. Find and validate alternate mount points.
+
+ * usr/share/time-slider/lib/time_slider/applet.py:
+ Refactored to allow more modular notification management.
+ Improved rsync device monitoring by implementing both gio.File
+ and gio.Volume monitors so we can track mounting/unmounting of
+ static mounts like nfs/zfs and variable mount points for
+ hotpluggable devices. Validate rsync target is correct using
+ SMF configuration key. Listen to TimeSlider.Config D-Bus events
+ and refresh configuration when D-Bus notification is received.
+ Add menu item to enable launching of setup GUI from the applet.
+
+ * usr/share/time-slider/lib/time_slider/dbussvc.py:
+ Add class and method definition for TimeSlider Config service
+ notifications.
+
+ * usr/share/time-slider/lib/time_slider/setupgui.py:
+ Register with system D-Bus and send notification of changes
+ to time-slider and rsync plugin services so the applet can
+ refresh.
+
2010-05-13 Niall Power - <niall.power@oracle.com>
* usr/share/time-slider/lib/plugin/rsync/backup.py:
View
14 etc/dbus-1/system.d/time-slider.conf
@@ -19,6 +19,14 @@
<allow own="org.opensolaris.TimeSlider.plugin.rsync"/>
</policy>
+ <!-- Only root or user zfssnap can own the config service -->
+ <policy user="zfssnap">
+ <allow own="org.opensolaris.TimeSlider.config"/>
+ </policy>
+ <policy user="root">
+ <allow own="org.opensolaris.TimeSlider.config"/>
+ </policy>
+
<!-- Allow anyone to invoke methods on time-sliderd -->
<policy context="default">
<allow send_destination="org.opensolaris.TimeSlider"/>
@@ -31,4 +39,10 @@
<allow receive_sender="org.opensolaris.TimeSlider.plugin.rsync"/>
</policy>
+ <!-- Allow anyone to invoke methods on the config service -->
+ <policy context="default">
+ <allow send_destination="org.opensolaris.TimeSlider.config"/>
+ <allow receive_sender="org.opensolaris.TimeSlider.config"/>
+ </policy>
+
</busconfig>
View
125 usr/share/time-slider/lib/plugin/rsync/backup.py
@@ -32,6 +32,7 @@
import math
import syslog
import gobject
+import gio
import dbus
import shutil
import copy
@@ -255,22 +256,20 @@ def __init__(self, fmri, dbus, mainLoop=None):
"<rsync/cleanup_threshold>: %d." \
"Using default value of 95%" \
% (self._cleanupThreshold))
- # Determine the rsync backup dir. This is the target dir
- # defined by the SMF instance plus the "TIMESLIDER/<nodename>"
- # suffix
- self._rsyncBaseDir = self._smfInst.get_target_dir()
- sys,nodeName,rel,ver,arch = os.uname()
- self._rsyncDir = os.path.join(self._rsyncBaseDir,
- rsyncsmf.RSYNCDIRPREFIX,
- nodeName)
- self._keyFile = os.path.join(self._rsyncBaseDir,
- rsyncsmf.RSYNCDIRPREFIX,
- rsyncsmf.RSYNCCONFIGFILE)
+
+ # Base variables for backup device. Will be initialised
+ # later in _find_backup_device()
self._smfTargetKey = self._smfInst.get_target_key()
+ sys,self._nodeName,rel,ver,arch = os.uname()
+ self._rsyncBaseDir = None
+ self._rsyncDir = None
+ self._keyFile = None
+
tsSMF = timeslidersmf.TimeSliderSMF()
self._labelSeparator = tsSMF.get_separator()
del tsSMF
-
+ # Finally go look for the backup device
+ self._find_backup_device()
def empty_trash_folders(self):
trashDirs = []
@@ -412,6 +411,73 @@ def _discover_backups(self):
insort(self._backups, [long(mtime), os.path.abspath(d)])
self._backupTimes[dirName][d] = mtime
+ def _find_backup_device(self):
+ # Determine the rsync backup dir. This is the target dir
+ # defined by the SMF instance plus the "TIMESLIDER/<nodename>"
+ # suffix. Try finding it at the preconfigured path first,
+ # then failing that, scan removable media mounts, in case it
+ # got remounted under a different path than at setup time.
+ self._rsyncBaseDir = None
+ path = self._smfInst.get_target_dir()
+ if self._validate_rsync_target(path) == True:
+ self._rsyncBaseDir = path
+ util.debug("Backup target device online: %s" % (path),
+ self._verbose)
+ else:
+ util.debug("Backup target device not mounted at: %s" \
+ "Scanning removable devices.." \
+ % (path),
+ self._verbose)
+ volMonitor = gio.volume_monitor_get()
+ mounts = volMonitor.get_mounts()
+ for mount in mounts:
+ root = mount.get_root()
+ path = root.get_path()
+ if self._validate_rsync_target(path) == True:
+ util.debug("Located backup target device at: %s" \
+ % (path),
+ self._verbose)
+ self._rsyncBaseDir = path
+
+ if self._rsyncBaseDir != None:
+ self._rsyncDir = os.path.join(self._rsyncBaseDir,
+ rsyncsmf.RSYNCDIRPREFIX,
+ self._nodeName)
+ self._keyFile = os.path.join(self._rsyncBaseDir,
+ rsyncsmf.RSYNCDIRPREFIX,
+ rsyncsmf.RSYNCCONFIGFILE)
+
+
+ def _validate_rsync_target(self, path):
+ """
+ Tests path to see if it is the pre-configured
+ rsync backup device path.
+ Returns True on success, otherwise False
+ """
+ # FIXME - this is duplicate in the applet and should
+ # be moved into a shared module
+ if not os.path.exists(path):
+ return False
+ testDir = os.path.join(path,
+ rsyncsmf.RSYNCDIRPREFIX,
+ self._nodeName)
+ testKeyFile = os.path.join(path,
+ rsyncsmf.RSYNCDIRPREFIX,
+ rsyncsmf.RSYNCCONFIGFILE)
+ if os.path.exists(testDir) and \
+ os.path.exists(testKeyFile):
+ testKeyVal = None
+ f = open(testKeyFile, 'r')
+ for line in f.readlines():
+ key, val = line.strip().split('=')
+ if key.strip() == "target_key":
+ targetKey = val.strip()
+ break
+ f.close()
+ if targetKey == self._smfTargetKey:
+ return True
+ return False
+
def _find_deleteable_backups(self, timestamp):
"""
Returns a list of backup directory paths that are older than
@@ -585,42 +651,19 @@ def _recover_space(self, deleteables):
def backup_snapshot(self):
# First, check to see if the rsync destination
# directory is accessible.
- try:
- os.stat(self._rsyncDir)
- except OSError:
- util.debug("Backup target directory is not " \
+
+ if self._rsyncBaseDir == None:
+ util.debug("Backup target device is not " \
"accessible right now: %s" \
- % (self._rsyncDir),
+ % (self._smfInst.get_target_dir()),
self._verbose)
self._bus.rsync_unsynced(len(self._pendingList))
if self._mainLoop:
self._mainLoop.quit()
sys.exit(0)
- # Check its config key matches what's recorded in SMF
- try:
- os.stat(self._keyFile)
- except OSError:
- util.debug("Backup target directory has no" \
- "configuration file. Possibly wrong " \
- "or misconfigured device: %s" \
- % (self._keyFile),
- self._verbose)
- self._bus.rsync_unsynced(len(self._pendingList))
- if self._mainLoop:
- self._mainLoop.quit()
- sys.exit(0)
-
- targetKey = None
- f = open(self._keyFile, 'r')
- for line in f.readlines():
- key, val = line.strip().split('=')
- if key.strip() == "target_key":
- targetKey = val.strip()
- break
- f.close()
-
- if targetKey != self._smfTargetKey:
+ # Extra paranoia
+ if self._validate_rsync_target(self._rsyncBaseDir) == False:
util.debug("Backup target directory does not " \
"have a matching configuration key. " \
"Possibly old or wrong device: %s" \
View
342 usr/share/time-slider/lib/time_slider/applet.py
@@ -48,8 +48,6 @@ class Note:
def __init__(self, icon, menu):
self._note = None
self._msgDialog = None
- self._cleanup_head = None
- self._cleanup_body = None
self._menu = menu
self._icon = icon
self._icon.connect("popup-menu", self._activate_menu)
@@ -79,6 +77,9 @@ def _show_notification(self):
def _connect_to_object(self):
pass
+ def refresh(self):
+ pass
+
def _watch_handler(self, new_owner = None):
if new_owner == None or len(new_owner) == 0:
pass
@@ -103,71 +104,188 @@ def __init__(self, icon, menu):
self.smfInst = rsyncsmf.RsyncSMF("%s:rsync" \
% (plugin.PLUGINBASEFMRI))
self._lock = threading.Lock()
+ self._masterKey = None
+ sys,self._nodeName,rel,ver,arch = os.uname()
+ # References to gio.File and handler_id of a registered
+ # monitor callback on gio.File
+ self._fm = None
+ self._fmID = None
+ # References to gio.VolumeMonitor and handler_ids of
+ # registered mount-added and mount-removed callbacks.
+ self._vm = None
+ self._vmAdd = None
+ self._vmRem = None
# Every time the rsync backup script runs it will
# register with d-bus and trigger self._watch_handler().
# Use this variable to keep track of it's running status.
self._scriptRunning = False
+ self._targetDirAvail = False
# If the user is authorised, allow them to manually synchronise
# rsync backups.
self._syncNowItem = None
if os.geteuid() == 0 or \
rbac.RBACprofile().has_profile("Primary Administrator"):
- self._syncNowItem = gtk.MenuItem(_("Synchronise Now"))
+ self._syncNowItem = gtk.MenuItem(_("Update Backups Now"))
self._syncNowItem.set_sensitive(False)
self._syncNowItem.connect("activate",
self._sync_now)
self._menu.append(self._syncNowItem)
- self._syncNowItem.show()
+ self.refresh()
- self._setup_file_monitor()
- # Kick start things by initially obtaining the
- # backlog size and triggering a callback.
- # Signal handlers will keep tooltip status up
- # to date afterwards when the backup cron job
- # executes.
- propName = "%s:rsync" % (backup.propbasename)
- queue = backup.list_pending_snapshots(propName)
- self.queueSize = len(queue)
- if self.queueSize == 0:
- self._rsync_synced_handler()
+ def _validate_rsync_target(self, path):
+ """
+ Tests path to see if it is the pre-configured
+ rsync backup device path.
+ Returns True on success, otherwise False
+ """
+ if not os.path.exists(path):
+ return False
+ testDir = join(path,
+ rsyncsmf.RSYNCDIRPREFIX,
+ self._nodeName)
+ testKeyFile = join(path,
+ rsyncsmf.RSYNCDIRPREFIX,
+ rsyncsmf.RSYNCCONFIGFILE)
+ if os.path.exists(testDir) and \
+ os.path.exists(testKeyFile):
+ testKeyVal = None
+ f = open(testKeyFile, 'r')
+ for line in f.readlines():
+ key, val = line.strip().split('=')
+ if key.strip() == "target_key":
+ targetKey = val.strip()
+ break
+ f.close()
+ if targetKey == self._masterKey:
+ return True
+ return False
+
+ def _setup_monitor(self):
+ # Disconnect any previously registered signal
+ # handlers
+ if self._fm:
+ self._fm.disconnect(self._fmID)
+ self._fm = None
+
+ useVolMonitor = False
+
+ # We always compare against masterKey to validate
+ # an rsync backup device.
+ self._masterKey = self.smfInst.get_target_key()
+ self._baseTargetDir = None
+ online = False
+
+ self._masterTargetDir = self.smfInst.get_target_dir()
+
+ if self._validate_rsync_target(self._masterTargetDir) == True:
+ self._baseTargetDir = self._masterTargetDir
+ online = True
+
+ if self._vm == None:
+ self._vm = gio.volume_monitor_get()
+
+ # If located, see if it's also managed by the volume monitor.
+ # Or just try to find it otherwise.
+ mounts = self._vm.get_mounts()
+ for mount in mounts:
+ root = mount.get_root()
+ path = root.get_path()
+ if self._baseTargetDir != None and \
+ path == self._baseTargetDir:
+ # Means the directory we found is gio monitored,
+ # so just monitor it using gio.VolumeMonitor.
+ useVolMonitor = True
+ break
+ elif self._validate_rsync_target(path) == True:
+ # Found it but not where we expected it to be so override
+ # the target path defined by SMF for now.
+ useVolMonitor = True
+ self._baseTargetDir = path
+ online = True
+ break
+
+ if self._baseTargetDir == None:
+ # Means we didn't find it, and we don't know where to expect
+ # it either - via a hotpluggable device or other nfs/zfs etc.
+ # We need to hedge our bets and monitor for both.
+ self._setup_file_monitor(self._masterTargetDir)
+ self._setup_volume_monitor()
else:
- self._rsync_unsynced_handler(self.queueSize)
-
- def _setup_file_monitor(self):
- self._baseTargetDir = self.smfInst.get_target_dir()
- sys,nodeName,rel,ver,arch = os.uname()
- self._targetDir = join(self._baseTargetDir,
- rsyncsmf.RSYNCDIRPREFIX,
- nodeName)
- self._targetDirAvail = False
+ # Found it
+ if useVolMonitor == True:
+ # Looks like a removable device. Use gio.VolumeMonitor
+ # as the preferred monitoring mechanism.
+ self._setup_volume_monitor()
+ else:
+ # Found it on a static mount point like a zfs or nfs
+ # mount point.
+ # Can't use gio.VolumeMonitor so use a gio.File monitor
+ # instead.
+ self._setup_file_monitor(self._masterTargetDir)
+ # Finally, update the UI menu state
self._lock.acquire()
- try:
- os.stat(self._targetDir)
- self._targetDirAvail = True
- except OSError:
- pass
+ self._targetDirAvail = online
+ self._update_menu_state()
self._lock.release()
-
- gFile = gio.File(path=self._baseTargetDir)
- self._monitor = gFile.monitor_file(gio.FILE_MONITOR_WATCH_MOUNTS)
- self._monitor.connect("changed", self._target_dir_changed)
-
- def _target_dir_changed(self, filemonitor, file, other_file, event_type):
-
- # It seems that FILE_MONITOR_EVENT_UNMOUNTED is unreliable
- # and is generated when a non-priviliged user attempts to
- # unmount a zfs file system. So check the status
- # of the target dir using os.stat() to confirm.
- self._lock.acquire()
- try:
- os.stat(self._targetDir)
+
+
+ def _setup_file_monitor(self, expectedPath):
+ # Use gio.File monitor as a fallback in
+ # case gio.VolumeMonitor can't track the device.
+ # This is the case for static/manual mount points
+ # such as NFS, ZFS and other non-hotpluggables.
+ gFile = gio.File(path=expectedPath)
+ self._fm = gFile.monitor_file(gio.FILE_MONITOR_WATCH_MOUNTS)
+ self._fmID = self._fm.connect("changed",
+ self._file_monitor_changed)
+
+ def _setup_volume_monitor(self):
+ # Check the handler_ids first to see if they have
+ # already been connected. Avoids multiple callbacks
+ # for a single event
+ if self._vmAdd == None:
+ self._vmAdd = self._vm.connect("mount-added",
+ self._mount_added)
+ if self._vmRem == None:
+ self._vmRem = self._vm.connect("mount-removed",
+ self._mount_removed)
+
+ def _mount_added(self, monitor, mount):
+ root = mount.get_root()
+ path = root.get_path()
+ if self._validate_rsync_target(path) == True:
+ # Since gio.VolumeMonitor found the rsync target, don't
+ # bother relying on gio.File to find it any more. Disconnect
+ # it's registered callbacks.
+ if self._fm:
+ self._fm.disconnect(self._fmID)
+ self._fm = None
+ self._lock.acquire()
+ self._baseTargetDir = path
self._targetDirAvail = True
- except OSError:
+ self._update_menu_state()
+ self._lock.release()
+
+ def _mount_removed(self, monitor, mount):
+ root = mount.get_root()
+ path = root.get_path()
+ if path == self._baseTargetDir:
+ self._lock.acquire()
self._targetDirAvail = False
- self._update_menu_state()
- self._lock.release()
+ self._update_menu_state()
+ self._lock.release()
+
+ def _file_monitor_changed(self, filemonitor, file, other_file, event_type):
+ if file.get_path() == self._masterTargetDir:
+ self._lock.acquire()
+ if self._validate_rsync_target(self._masterTargetDir) == True:
+ self._targetDirAvail = True
+ else:
+ self._targetDirAvail = False
+ self._update_menu_state()
+ self._lock.release()
def _update_menu_state(self):
if self._syncNowItem:
@@ -236,8 +354,8 @@ def _connect_to_object(self):
remote_object = bus.get_object("org.opensolaris.TimeSlider.plugin.rsync",
"/org/opensolaris/TimeSlider/plugin/rsync")
except dbus.DBusException:
- print "Failed to connect to remote D-Bus object: %s" % \
- ("/org/opensolaris/TimeSlider/plugin/rsync")
+ sys.stderr.write("Failed to connect to remote D-Bus object: " + \
+ "/org/opensolaris/TimeSlider/plugin/rsync")
return
# Create an Interface wrapper for the remote object
@@ -254,6 +372,28 @@ def _connect_to_object(self):
iface.connect_to_signal("rsync_unsynced", self._rsync_unsynced_handler, sender_keyword='sender',
interface_keyword='interface', path_keyword='path')
+ def refresh(self):
+ # Hide/Unhide rsync menu item based on whether the plugin is online
+ if self._syncNowItem and \
+ self.smfInst.get_service_state() == "online":
+ #self._setup_file_monitor()
+ self._setup_monitor()
+ # Kick start things by initially obtaining the
+ # backlog size and triggering a callback.
+ # Signal handlers will keep tooltip status up
+ # to date afterwards when the backup cron job
+ # executes.
+ propName = "%s:rsync" % (backup.propbasename)
+ queue = backup.list_pending_snapshots(propName)
+ self.queueSize = len(queue)
+ if self.queueSize == 0:
+ self._rsync_synced_handler()
+ else:
+ self._rsync_unsynced_handler(self.queueSize)
+ self._syncNowItem.show()
+ else:
+ self._syncNowItem.hide()
+
def _sync_now(self, menuItem):
"""Runs the rsync-bacjup script manually
Assumes that user is either root or has the
@@ -271,6 +411,8 @@ class CleanupNote(Note):
def __init__(self, icon, menu):
Note.__init__(self, icon, menu)
+ self._cleanupHead = None
+ self._cleanupBody = None
dbus.bus.NameOwnerWatch(bus,
"org.opensolaris.TimeSlider",
self._watch_handler)
@@ -281,8 +423,8 @@ def _show_cleanup_details(self, *args):
dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING,
buttons=gtk.BUTTONS_CLOSE)
dialog.set_title(_("Time Slider: Low Space Warning"))
- dialog.set_markup("<b>%s</b>" % (self._cleanup_head))
- dialog.format_secondary_markup(self._cleanup_body)
+ dialog.set_markup("<b>%s</b>" % (self._cleanupHead))
+ dialog.format_secondary_markup(self._cleanupBody)
dialog.show()
dialog.present()
dialog.connect("response", self._dialog_response)
@@ -291,10 +433,10 @@ def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=No
if severity == 4:
expiry = pynotify.EXPIRES_NEVER
urgency = pynotify.URGENCY_CRITICAL
- self._cleanup_head = _("Emergency: \'%s\' is full!") % pool
+ self._cleanupHead = _("Emergency: \'%s\' is full!") % pool
notifyBody = _("The file system: \'%s\', is over %s%% full.") \
% (pool, threshhold)
- self._cleanup_body = _("The file system: \'%s\', is over %s%% full.\n"
+ self._cleanupBody = _("The file system: \'%s\', is over %s%% full.\n"
"As an emergency measure, Time Slider has "
"destroyed all of its backups.\nTo fix this problem, "
"delete any unnecessary files on \'%s\', or add "
@@ -303,11 +445,11 @@ def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=No
elif severity == 3:
expiry = pynotify.EXPIRES_NEVER
urgency = pynotify.URGENCY_CRITICAL
- self._cleanup_head = _("Emergency: \'%s\' is almost full!") % pool
+ self._cleanupHead = _("Emergency: \'%s\' is almost full!") % pool
notifyBody = _("The file system: \'%s\', exceeded %s%% "
"of its total capacity") \
% (pool, threshhold)
- self._cleanup_body = _("The file system: \'%s\', exceeded %s%% "
+ self._cleanupBody = _("The file system: \'%s\', exceeded %s%% "
"of its total capacity. As an emerency measure, "
"Time Slider has has destroyed most or all of its "
"backups to prevent the disk becoming full. "
@@ -318,11 +460,11 @@ def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=No
elif severity == 2:
expiry = pynotify.EXPIRES_NEVER
urgency = pynotify.URGENCY_CRITICAL
- self._cleanup_head = _("Urgent: \'%s\' is almost full!") % pool
+ self._cleanupHead = _("Urgent: \'%s\' is almost full!") % pool
notifyBody = _("The file system: \'%s\', exceeded %s%% "
"of its total capacity") \
% (pool, threshhold)
- self._cleanup_body = _("The file system: \'%s\', exceeded %s%% "
+ self._cleanupBody = _("The file system: \'%s\', exceeded %s%% "
"of its total capacity. As a remedial measure, "
"Time Slider has destroyed some backups, and will "
"destroy more, eventually all, as capacity continues "
@@ -333,11 +475,11 @@ def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=No
elif severity == 1:
expiry = 20000 # 20 seconds
urgency = pynotify.URGENCY_NORMAL
- self._cleanup_head = _("Warning: \'%s\' is getting full") % pool
+ self._cleanupHead = _("Warning: \'%s\' is getting full") % pool
notifyBody = _("The file system: \'%s\', exceeded %s%% "
"of its total capacity") \
% (pool, threshhold)
- self._cleanup_body = _("\'%s\' exceeded %s%% of its total "
+ self._cleanupBody = _("\'%s\' exceeded %s%% of its total "
"capacity. To fix this, Time Slider has destroyed "
"some recent backups, and will destroy more as "
"capacity continues to diminish.\nTo prevent "
@@ -350,7 +492,7 @@ def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=No
if (self._note != None):
self._note.close()
- self._note = pynotify.Notification(self._cleanup_head,
+ self._note = pynotify.Notification(self._cleanupHead,
notifyBody)
self._note.add_action("clicked",
_("Details..."),
@@ -368,8 +510,8 @@ def _connect_to_object(self):
remote_object = bus.get_object("org.opensolaris.TimeSlider",
"/org/opensolaris/TimeSlider/autosnap")
except dbus.DBusException:
- print "Failed to connect to remote D-Bus object: %s" % \
- ("/org/opensolaris/TimeSlider/autosnap")
+ sys.stderr.write("Failed to connect to remote D-Bus object: " + \
+ "/org/opensolaris/TimeSlider/autosnap")
#Create an Interface wrapper for the remote object
iface = dbus.Interface(remote_object, "org.opensolaris.TimeSlider.autosnap")
@@ -378,6 +520,75 @@ def _connect_to_object(self):
interface_keyword='interface', path_keyword='path')
+
+class SetupNote(Note):
+
+ def __init__(self, icon, menu, manager):
+ Note.__init__(self, icon, menu)
+ # We are passed a reference to out parent so we can
+ # provide it notification which it can then circulate
+ # to other notification objects such as Rsync and
+ # Cleanup
+ self._manager = manager
+ self._icon = icon
+ self._menu = menu
+ self._configSvcItem = gtk.MenuItem(_("Configure Time Slider..."))
+ self._configSvcItem.connect("activate",
+ self._run_config_app)
+ self._configSvcItem.set_sensitive(True)
+ self._menu.append(self._configSvcItem)
+ self._configSvcItem.show()
+ dbus.bus.NameOwnerWatch(bus,
+ "org.opensolaris.TimeSlider.config",
+ self._watch_handler)
+
+ def _connect_to_object(self):
+ try:
+ remote_object = bus.get_object("org.opensolaris.TimeSlider.config",
+ "/org/opensolaris/TimeSlider/config")
+ except dbus.DBusException:
+ sys.stderr.write("Failed to connect to remote D-Bus object: " + \
+ "/org/opensolaris/TimeSlider/config")
+
+ #Create an Interface wrapper for the remote object
+ iface = dbus.Interface(remote_object, "org.opensolaris.TimeSlider.config")
+
+ iface.connect_to_signal("config_changed", self._config_handler, sender_keyword='sender',
+ interface_keyword='interface', path_keyword='path')
+
+ def _config_handler(self, sender=None, interface=None, path=None):
+ # Notify the manager.
+ # This will eventually propogate through to an invocation
+ # of our own refresh() method.
+ self._manager.refresh()
+
+ def _run_config_app(self, menuItem):
+ cmdPath = os.path.join(os.path.dirname(sys.argv[0]),
+ os.path.pardir,
+ "bin",
+ "time-slider-setup")
+ cmd = os.path.abspath(cmdPath)
+ # The setup GUI deals with it's own security and
+ # authorisation, so no need to pfexec it. Any
+ # changes made to configuration will come back to
+ # us by way of D-Bus notification.
+ subprocess.Popen(cmd, close_fds=True)
+
+class NoteManager():
+ def __init__(self):
+ # Notification objects need to share a common
+ # status icon and popup menu so these are created
+ # outside the object and passed to the constructor
+ self._menu = gtk.Menu()
+ self._icon = gtk.StatusIcon()
+ self._icon.set_from_icon_name("time-slider-setup")
+ self._setupNote = SetupNote(self._icon, self._menu, self)
+ self._cleanupNote = CleanupNote(self._icon, self._menu)
+ self._rsyncNote = RsyncNote(self._icon, self._menu)
+
+ def refresh(self):
+ self._rsyncNote.refresh()
+
bus = dbus.SystemBus()
def main(argv):
@@ -386,14 +597,7 @@ def main(argv):
gobject.threads_init()
pynotify.init(_("Time Slider"))
- # Notification objects need to share common
- # status icon and popup menu so these are created
- # outside the object and passed to the constructor
- menu = gtk.Menu()
- icon = gtk.StatusIcon()
- icon.set_from_icon_name("time-slider-setup")
- cleanupNote = CleanupNote(icon, menu)
- rsyncNote = RsyncNote(icon, menu)
+ noteManager = NoteManager()
try:
mainloop.run()
View
17 usr/share/time-slider/lib/time_slider/dbussvc.py
@@ -84,3 +84,20 @@ def rsync_synced(self):
def rsync_unsynced(self, queueSize):
pass
+
+class Config(dbus.service.Object):
+ """
+ D-Bus object representing Time Slider service configuration changes.
+ """
+ def __init__(self, bus, path):
+ self._bus = bus
+ dbus.service.Object.__init__(self,
+ bus,
+ path)
+ # Service configuration change signal. Nothing fancy for now.
+ # Listeners need to figure out what changed for themselves.
+ @dbus.service.signal(dbus_interface="org.opensolaris.TimeSlider.config",
+ signature='')
+ def config_changed(self):
+ pass
+
View
53 usr/share/time-slider/lib/time_slider/setupgui.py
@@ -45,11 +45,15 @@
gtk.gdk.threads_init()
except:
sys.exit(1)
-try:
- import glib
- import gobject
-except:
- sys.exit(1)
+
+import glib
+import gobject
+import dbus
+import dbus.service
+import dbus.mainloop
+import dbus.mainloop.glib
+import dbussvc
+
# This is the rough guess ratio used for rsync backup device size
# vs. the total size of the pools it's expected to backup.
@@ -113,6 +117,20 @@ def __init__(self, execpath):
self.xml = gtk.glade.XML("%s/../../glade/time-slider-setup.glade" \
% (os.path.dirname(__file__)))
+ # Tell dbus to use the gobject mainloop for async ops
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ dbus.mainloop.glib.threads_init()
+
+ # Register a bus name with the system dbus daemon
+ systemBus = dbus.SystemBus()
+ busName = dbus.service.BusName("org.opensolaris.TimeSlider.config",
+ systemBus)
+ self._dbus = dbussvc.Config(systemBus,
+ '/org/opensolaris/TimeSlider/config')
+ # Used later to trigger a D-Bus notification of select configuration
+ # changes made
+ self._configNotify = False
+
# These variables record the initial UI state which are used
# later to compare against the UI state when the OK or Cancel
# button is clicked and apply the minimum set of necessary
@@ -731,7 +749,9 @@ def __on_ok_clicked(self, widget):
if self._initialEnabledState == True:
self.sliderSMF.disable_service()
# Ignore other changes to the snapshot/rsync configuration
- # of filesystems. Just exit.
+ # of filesystems. Just broadcast the change and exit.
+ self._configNotify = True
+ self.broadcast_changes()
gtk.main_quit()
else:
model = self.fstv.get_model()
@@ -927,23 +947,30 @@ def setup_rsync_config(self):
sys.exit(-1)
self.rsyncSMF.set_target_dir(self.rsyncTargetDir)
self.rsyncSMF.set_target_key(newKey)
+ # Applet monitors rsyncTargetDir so make sure to notify it.
+ self._configNotify = True
return
def setup_services(self):
- # Take care of the rsync plugin service first
- # since time-slider will query it.
+ # Take care of the rsync plugin service first since time-slider
+ # will query it.
+ # Changes to rsync or time-slider SMF service State should be
+ # broadcast to let notification applet refresh.
if self.rsyncEnabled == True and \
self._initialRsyncState == False:
self.rsyncSMF.enable_service()
+ self._configNotify = True
elif self.rsyncEnabled == False and \
self._initialRsyncState == True:
self.rsyncSMF.disable_service()
+ self._configNotify = True
customSelection = self.xml.get_widget("selectfsradio").get_active()
if customSelection != self._initialCustomSelection:
self.sliderSMF.set_custom_selection(customSelection)
if self._initialEnabledState == False:
enable_default_schedules()
self.sliderSMF.enable_service()
+ self._configNotify = True
def set_cleanup_level(self):
"""
@@ -954,6 +981,15 @@ def set_cleanup_level(self):
if level != self._initialCleanupLevel:
self.sliderSMF.set_cleanup_level("warning", level)
+ def broadcast_changes(self):
+ """
+ Blunt instrument to notify D-Bus listeners such as notification
+ applet to rescan service configuration
+ """
+ if self._configNotify == False:
+ return
+ self._dbus.config_changed()
+
def __on_deletesnapshots_clicked(self, widget):
cmdpath = os.path.join(os.path.dirname(self.execpath), \
"../lib/time-slider-delete")
@@ -976,6 +1012,7 @@ def run(self):
self._setupManager.setup_rsync_config()
self._setupManager.setup_services()
+ self._setupManager.broadcast_changes()
except RuntimeError, message:
sys.stderr.write(str(message))

0 comments on commit d9df954

Please sign in to comment.