#!/usr/bin/python2 # # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (the "License"). # You may not use this file except in compliance with the License. # # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE # or http://www.opensolaris.org/os/licensing. # See the License for the specific language governing permissions # and limitations under the License. # # When distributing Covered Code, include this CDDL HEADER in each # file and include the License file at usr/src/OPENSOLARIS.LICENSE. # If applicable, add the following below this CDDL HEADER, with the # fields enclosed by brackets "[]" replaced with your own identifying # information: Portions Copyright [yyyy] [name of copyright owner] # # CDDL HEADER END # import sys import os import subprocess import threading import gobject import dbus import dbus.decorators import dbus.glib import dbus.mainloop import dbus.mainloop.glib import gio import gtk import pygtk import pynotify from time_slider import util, rbac from os.path import abspath, dirname, join, pardir sys.path.insert(0, join(dirname(__file__), pardir, "plugin")) import plugin sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync")) import backup, rsyncsmf class Note: _iconConnected = False def __init__(self, icon, menu): self._note = None self._msgDialog = None self._menu = menu self._icon = icon if Note._iconConnected == False: self._icon.connect("popup-menu", self._popup_menu) Note._iconConnected = True self._icon.set_visible(True) def _popup_menu(self, icon, button, time): if button == 3: # Don't popup an empty menu if len(self._menu.get_children()) > 0: self._menu.popup(None, None, gtk.status_icon_position_menu, button, time, icon) def _dialog_response(self, dialog, response): dialog.destroy() def _notification_closed(self, notifcation): self._note = None self._icon.set_blinking(False) def _show_notification(self): if self._icon.is_embedded() == True: self._note.attach_to_status_icon(self._icon) self._note.show() return False 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 else: self._connect_to_object() def _setup_icon_for_note(self, themed=None): if themed: iconList = themed.get_names() else: iconList = ['gnome-dev-harddisk'] iconTheme = gtk.icon_theme_get_default() iconInfo = iconTheme.choose_icon(iconList, 48, 0) pixbuf = iconInfo.load_icon() self._note.set_category("device") self._note.set_icon_from_pixbuf(pixbuf) class RsyncNote(Note): def __init__(self, icon, menu): Note.__init__(self, icon, menu) dbus.bus.NameOwnerWatch(bus, "org.opensolaris.TimeSlider.plugin.rsync", self._watch_handler) 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 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.refresh() 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: # 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() self._targetDirAvail = online self._update_menu_state() self._lock.release() 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 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() 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: if self._targetDirAvail == True and \ self._scriptRunning == False: self._syncNowItem.set_sensitive(True) else: self._syncNowItem.set_sensitive(False) def _watch_handler(self, new_owner = None): self._lock.acquire() if new_owner == None or len(new_owner) == 0: # Script not running or exited self._scriptRunning = False else: self._scriptRunning = True self._connect_to_object() self._update_menu_state() self._lock.release() def _rsync_started_handler(self, target, sender=None, interface=None, path=None): urgency = pynotify.URGENCY_NORMAL if (self._note != None): self._note.close() # Try to pretty things up a bit by displaying volume name # and hinted icon instead of the raw device path, # and standard harddisk icon if possible. icon = None volume = util.path_to_volume(target) if volume == None: volName = target else: volName = volume.get_name() icon = volume.get_icon() self._note = pynotify.Notification(_("Backup Started"), _("Backing up snapshots to:\n%s\n" \ "Do not disconnect the backup device.") \ % (volName)) self._note.connect("closed", \ self._notification_closed) self._note.set_urgency(urgency) self._setup_icon_for_note(icon) gobject.idle_add(self._show_notification) def _rsync_current_handler(self, snapshot, remaining, sender=None, interface=None, path=None): self._icon.set_tooltip_markup(_("Backing up: \'%s\'\n%d snapshots remaining.\n" \ "Do not disconnect the backup device.") \ % (snapshot, remaining)) def _rsync_complete_handler(self, target, sender=None, interface=None, path=None): urgency = pynotify.URGENCY_NORMAL if (self._note != None): self._note.close() # Try to pretty things up a bit by displaying volume name # and hinted icon instead of the raw device path, # and standard harddisk icon if possible. icon = None volume = util.path_to_volume(target) if volume == None: volName = target else: volName = volume.get_name() icon = volume.get_icon() self._note = pynotify.Notification(_("Backup Complete"), _("Your snapshots have been backed up to:\n%s") \ % (volName)) self._note.connect("closed", \ self._notification_closed) self._note.set_urgency(urgency) self._setup_icon_for_note(icon) self._icon.set_has_tooltip(False) self.queueSize = 0 gobject.idle_add(self._show_notification) def _rsync_synced_handler(self, sender=None, interface=None, path=None): self._icon.set_tooltip_markup(_("Your backups are up to date.")) self.queueSize = 0 def _rsync_unsynced_handler(self, queueSize, sender=None, interface=None, path=None): self._icon.set_tooltip_markup(_("%d snapshots are queued for backup.") \ % (queueSize)) self.queueSize = queueSize def _connect_to_object(self): try: remote_object = bus.get_object("org.opensolaris.TimeSlider.plugin.rsync", "/org/opensolaris/TimeSlider/plugin/rsync") except dbus.DBusException: 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 iface = dbus.Interface(remote_object, "org.opensolaris.TimeSlider.plugin.rsync") iface.connect_to_signal("rsync_started", self._rsync_started_handler, sender_keyword='sender', interface_keyword='interface', path_keyword='path') iface.connect_to_signal("rsync_current", self._rsync_current_handler, sender_keyword='sender', interface_keyword='interface', path_keyword='path') iface.connect_to_signal("rsync_complete", self._rsync_complete_handler, sender_keyword='sender', interface_keyword='interface', path_keyword='path') iface.connect_to_signal("rsync_synced", self._rsync_synced_handler, sender_keyword='sender', interface_keyword='interface', path_keyword='path') 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-backup script manually Assumes that user is root since it is only called from the menu item which is invisible to not authorised users """ cmdPath = os.path.join(os.path.dirname(sys.argv[0]), \ "time-slider/plugins/rsync/rsync-backup") if os.geteuid() == 0: cmd = [cmdPath, \ "%s:rsync" % (plugin.PLUGINBASEFMRI)] else: cmd = ['/usr/bin/gksu' ,cmdPath, \ "%s:rsync" % (plugin.PLUGINBASEFMRI)] subprocess.Popen(cmd, close_fds=True, cwd="/") 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) def _show_cleanup_details(self, *args): # We could keep a dialog around but this a rare # enough event that's it not worth the effort. dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE) dialog.set_title(_("Time Slider: Low Space Warning")) dialog.set_markup("%s" % (self._cleanupHead)) dialog.format_secondary_markup(self._cleanupBody) dialog.show() dialog.present() dialog.connect("response", self._dialog_response) def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=None, path=None): if severity == 4: expiry = pynotify.EXPIRES_NEVER urgency = pynotify.URGENCY_CRITICAL self._cleanupHead = _("Emergency: \'%s\' is full!") % pool notifyBody = _("The file system: \'%s\', is over %s%% full.") \ % (pool, threshhold) 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 " "disk space (see ZFS documentation).") \ % (pool, threshhold, pool) elif severity == 3: expiry = pynotify.EXPIRES_NEVER urgency = pynotify.URGENCY_CRITICAL self._cleanupHead = _("Emergency: \'%s\' is almost full!") % pool notifyBody = _("The file system: \'%s\', exceeded %s%% " "of its total capacity") \ % (pool, threshhold) 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. " "To prevent this from happening again, delete " "any unnecessary files on \'%s\', or add disk " "space (see ZFS documentation).") \ % (pool, threshhold, pool) elif severity == 2: expiry = pynotify.EXPIRES_NEVER urgency = pynotify.URGENCY_CRITICAL self._cleanupHead = _("Urgent: \'%s\' is almost full!") % pool notifyBody = _("The file system: \'%s\', exceeded %s%% " "of its total capacity") \ % (pool, threshhold) 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 " "to diminish.\nTo prevent this from happening again, " "delete any unnecessary files on \'%s\', or add disk " "space (see ZFS documentation).") \ % (pool, threshhold, pool) elif severity == 1: expiry = 20000 # 20 seconds urgency = pynotify.URGENCY_NORMAL self._cleanupHead = _("Warning: \'%s\' is getting full") % pool notifyBody = _("The file system: \'%s\', exceeded %s%% " "of its total capacity") \ % (pool, threshhold) 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 " "this from happening again, delete any " "unnecessary files on \'%s\', or add disk space " "(see ZFS documentation).\n") \ % (pool, threshhold, pool) else: return # No other values currently supported if (self._note != None): self._note.close() self._note = pynotify.Notification(self._cleanupHead, notifyBody) self._note.add_action("clicked", _("Details..."), self._show_cleanup_details) self._note.connect("closed", self._notification_closed) self._note.set_urgency(urgency) self._note.set_timeout(expiry) self._setup_icon_for_note() self._icon.set_blinking(True) gobject.idle_add(self._show_notification) def _connect_to_object(self): try: remote_object = bus.get_object("org.opensolaris.TimeSlider", "/org/opensolaris/TimeSlider/autosnap") except dbus.DBusException: 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") iface.connect_to_signal("capacity_exceeded", self._cleanup_handler, sender_keyword='sender', 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): mainloop = gobject.MainLoop() dbus.mainloop.glib.DBusGMainLoop(set_as_default = True) gobject.threads_init() pynotify.init(_("Time Slider")) noteManager = NoteManager() try: mainloop.run() except: print "Exiting" if __name__ == '__main__': main()