5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
29 import dbus.decorators
32 import dbus.mainloop.glib
38 from time_slider import util, rbac
40 from os.path import abspath, dirname, join, pardir
41 sys.path.insert(0, join(dirname(__file__), pardir, "plugin"))
43 sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync"))
44 import backup, rsyncsmf
47 _iconConnected = False
49 def __init__(self, icon, menu):
51 self._msgDialog = None
54 if Note._iconConnected == False:
55 self._icon.connect("popup-menu", self._popup_menu)
56 Note._iconConnected = True
57 self._icon.set_visible(True)
59 def _popup_menu(self, icon, button, time):
61 # Don't popup an empty menu
62 if len(self._menu.get_children()) > 0:
63 self._menu.popup(None, None,
64 gtk.status_icon_position_menu,
67 def _dialog_response(self, dialog, response):
70 def _notification_closed(self, notifcation):
72 self._icon.set_blinking(False)
74 def _show_notification(self):
75 if self._icon.is_embedded() == True:
76 self._note.attach_to_status_icon(self._icon)
80 def _connect_to_object(self):
86 def _watch_handler(self, new_owner = None):
87 if new_owner == None or len(new_owner) == 0:
90 self._connect_to_object()
92 def _setup_icon_for_note(self, themed=None):
94 iconList = themed.get_names()
96 iconList = ['gnome-dev-harddisk']
98 iconTheme = gtk.icon_theme_get_default()
99 iconInfo = iconTheme.choose_icon(iconList, 48, 0)
100 pixbuf = iconInfo.load_icon()
102 self._note.set_category("device")
103 self._note.set_icon_from_pixbuf(pixbuf)
106 class RsyncNote(Note):
108 def __init__(self, icon, menu):
109 Note.__init__(self, icon, menu)
110 dbus.bus.NameOwnerWatch(bus,
111 "org.opensolaris.TimeSlider.plugin.rsync",
114 self.smfInst = rsyncsmf.RsyncSMF("%s:rsync" \
115 % (plugin.PLUGINBASEFMRI))
116 self._lock = threading.Lock()
117 self._masterKey = None
118 sys,self._nodeName,rel,ver,arch = os.uname()
119 # References to gio.File and handler_id of a registered
120 # monitor callback on gio.File
123 # References to gio.VolumeMonitor and handler_ids of
124 # registered mount-added and mount-removed callbacks.
128 # Every time the rsync backup script runs it will
129 # register with d-bus and trigger self._watch_handler().
130 # Use this variable to keep track of it's running status.
131 self._scriptRunning = False
132 self._targetDirAvail = False
133 self._syncNowItem = gtk.MenuItem(_("Update Backups Now"))
134 self._syncNowItem.set_sensitive(False)
135 self._syncNowItem.connect("activate",
137 self._menu.append(self._syncNowItem)
141 def _validate_rsync_target(self, path):
143 Tests path to see if it is the pre-configured
144 rsync backup device path.
145 Returns True on success, otherwise False
147 if not os.path.exists(path):
150 rsyncsmf.RSYNCDIRPREFIX,
152 testKeyFile = join(path,
153 rsyncsmf.RSYNCDIRPREFIX,
154 rsyncsmf.RSYNCCONFIGFILE)
155 if os.path.exists(testDir) and \
156 os.path.exists(testKeyFile):
158 f = open(testKeyFile, 'r')
159 for line in f.readlines():
160 key, val = line.strip().split('=')
161 if key.strip() == "target_key":
162 targetKey = val.strip()
165 if targetKey == self._masterKey:
169 def _setup_monitor(self):
170 # Disconnect any previously registered signal
173 self._fm.disconnect(self._fmID)
176 useVolMonitor = False
178 # We always compare against masterKey to validate
179 # an rsync backup device.
180 self._masterKey = self.smfInst.get_target_key()
181 self._baseTargetDir = None
184 self._masterTargetDir = self.smfInst.get_target_dir()
186 if self._validate_rsync_target(self._masterTargetDir) == True:
187 self._baseTargetDir = self._masterTargetDir
191 self._vm = gio.volume_monitor_get()
193 # If located, see if it's also managed by the volume monitor.
194 # Or just try to find it otherwise.
195 mounts = self._vm.get_mounts()
197 root = mount.get_root()
198 path = root.get_path()
199 if self._baseTargetDir != None and \
200 path == self._baseTargetDir:
201 # Means the directory we found is gio monitored,
202 # so just monitor it using gio.VolumeMonitor.
205 elif self._validate_rsync_target(path) == True:
206 # Found it but not where we expected it to be so override
207 # the target path defined by SMF for now.
209 self._baseTargetDir = path
213 if self._baseTargetDir == None:
214 # Means we didn't find it, and we don't know where to expect
215 # it either - via a hotpluggable device or other nfs/zfs etc.
216 # We need to hedge our bets and monitor for both.
217 self._setup_file_monitor(self._masterTargetDir)
218 self._setup_volume_monitor()
221 if useVolMonitor == True:
222 # Looks like a removable device. Use gio.VolumeMonitor
223 # as the preferred monitoring mechanism.
224 self._setup_volume_monitor()
226 # Found it on a static mount point like a zfs or nfs
228 # Can't use gio.VolumeMonitor so use a gio.File monitor
230 self._setup_file_monitor(self._masterTargetDir)
232 # Finally, update the UI menu state
234 self._targetDirAvail = online
235 self._update_menu_state()
239 def _setup_file_monitor(self, expectedPath):
240 # Use gio.File monitor as a fallback in
241 # case gio.VolumeMonitor can't track the device.
242 # This is the case for static/manual mount points
243 # such as NFS, ZFS and other non-hotpluggables.
244 gFile = gio.File(path=expectedPath)
245 self._fm = gFile.monitor_file(gio.FILE_MONITOR_WATCH_MOUNTS)
246 self._fmID = self._fm.connect("changed",
247 self._file_monitor_changed)
249 def _setup_volume_monitor(self):
250 # Check the handler_ids first to see if they have
251 # already been connected. Avoids multiple callbacks
253 if self._vmAdd == None:
254 self._vmAdd = self._vm.connect("mount-added",
256 if self._vmRem == None:
257 self._vmRem = self._vm.connect("mount-removed",
260 def _mount_added(self, monitor, mount):
261 root = mount.get_root()
262 path = root.get_path()
263 if self._validate_rsync_target(path) == True:
264 # Since gio.VolumeMonitor found the rsync target, don't
265 # bother relying on gio.File to find it any more. Disconnect
266 # it's registered callbacks.
268 self._fm.disconnect(self._fmID)
271 self._baseTargetDir = path
272 self._targetDirAvail = True
273 self._update_menu_state()
276 def _mount_removed(self, monitor, mount):
277 root = mount.get_root()
278 path = root.get_path()
279 if path == self._baseTargetDir:
281 self._targetDirAvail = False
282 self._update_menu_state()
285 def _file_monitor_changed(self, filemonitor, file, other_file, event_type):
286 if file.get_path() == self._masterTargetDir:
288 if self._validate_rsync_target(self._masterTargetDir) == True:
289 self._targetDirAvail = True
291 self._targetDirAvail = False
292 self._update_menu_state()
295 def _update_menu_state(self):
296 if self._syncNowItem:
297 if self._targetDirAvail == True and \
298 self._scriptRunning == False:
299 self._syncNowItem.set_sensitive(True)
301 self._syncNowItem.set_sensitive(False)
303 def _watch_handler(self, new_owner = None):
305 if new_owner == None or len(new_owner) == 0:
306 # Script not running or exited
307 self._scriptRunning = False
309 self._scriptRunning = True
310 self._connect_to_object()
311 self._update_menu_state()
314 def _rsync_started_handler(self, target, sender=None, interface=None, path=None):
315 urgency = pynotify.URGENCY_NORMAL
316 if (self._note != None):
318 # Try to pretty things up a bit by displaying volume name
319 # and hinted icon instead of the raw device path,
320 # and standard harddisk icon if possible.
322 volume = util.path_to_volume(target)
326 volName = volume.get_name()
327 icon = volume.get_icon()
329 self._note = pynotify.Notification(_("Backup Started"),
330 _("Backing up snapshots to:\n<b>%s</b>\n" \
331 "Do not disconnect the backup device.") \
333 self._note.connect("closed", \
334 self._notification_closed)
335 self._note.set_urgency(urgency)
336 self._setup_icon_for_note(icon)
337 gobject.idle_add(self._show_notification)
339 def _rsync_current_handler(self, snapshot, remaining, sender=None, interface=None, path=None):
340 self._icon.set_tooltip_markup(_("Backing up: <b>\'%s\'\n%d</b> snapshots remaining.\n" \
341 "Do not disconnect the backup device.") \
342 % (snapshot, remaining))
344 def _rsync_complete_handler(self, target, sender=None, interface=None, path=None):
345 urgency = pynotify.URGENCY_NORMAL
346 if (self._note != None):
348 # Try to pretty things up a bit by displaying volume name
349 # and hinted icon instead of the raw device path,
350 # and standard harddisk icon if possible.
352 volume = util.path_to_volume(target)
356 volName = volume.get_name()
357 icon = volume.get_icon()
359 self._note = pynotify.Notification(_("Backup Complete"),
360 _("Your snapshots have been backed up to:\n<b>%s</b>") \
362 self._note.connect("closed", \
363 self._notification_closed)
364 self._note.set_urgency(urgency)
365 self._setup_icon_for_note(icon)
366 self._icon.set_has_tooltip(False)
368 gobject.idle_add(self._show_notification)
370 def _rsync_synced_handler(self, sender=None, interface=None, path=None):
371 self._icon.set_tooltip_markup(_("Your backups are up to date."))
374 def _rsync_unsynced_handler(self, queueSize, sender=None, interface=None, path=None):
375 self._icon.set_tooltip_markup(_("%d snapshots are queued for backup.") \
377 self.queueSize = queueSize
379 def _connect_to_object(self):
381 remote_object = bus.get_object("org.opensolaris.TimeSlider.plugin.rsync",
382 "/org/opensolaris/TimeSlider/plugin/rsync")
383 except dbus.DBusException:
384 sys.stderr.write("Failed to connect to remote D-Bus object: " + \
385 "/org/opensolaris/TimeSlider/plugin/rsync")
388 # Create an Interface wrapper for the remote object
389 iface = dbus.Interface(remote_object, "org.opensolaris.TimeSlider.plugin.rsync")
391 iface.connect_to_signal("rsync_started", self._rsync_started_handler, sender_keyword='sender',
392 interface_keyword='interface', path_keyword='path')
393 iface.connect_to_signal("rsync_current", self._rsync_current_handler, sender_keyword='sender',
394 interface_keyword='interface', path_keyword='path')
395 iface.connect_to_signal("rsync_complete", self._rsync_complete_handler, sender_keyword='sender',
396 interface_keyword='interface', path_keyword='path')
397 iface.connect_to_signal("rsync_synced", self._rsync_synced_handler, sender_keyword='sender',
398 interface_keyword='interface', path_keyword='path')
399 iface.connect_to_signal("rsync_unsynced", self._rsync_unsynced_handler, sender_keyword='sender',
400 interface_keyword='interface', path_keyword='path')
403 # Hide/Unhide rsync menu item based on whether the plugin is online
404 if self._syncNowItem and \
405 self.smfInst.get_service_state() == "online":
406 #self._setup_file_monitor()
407 self._setup_monitor()
408 # Kick start things by initially obtaining the
409 # backlog size and triggering a callback.
410 # Signal handlers will keep tooltip status up
411 # to date afterwards when the backup cron job
413 propName = "%s:rsync" % (backup.propbasename)
414 queue = backup.list_pending_snapshots(propName)
415 self.queueSize = len(queue)
416 if self.queueSize == 0:
417 self._rsync_synced_handler()
419 self._rsync_unsynced_handler(self.queueSize)
420 self._syncNowItem.show()
422 self._syncNowItem.hide()
424 def _sync_now(self, menuItem):
425 """Runs the rsync-backup script manually
426 Assumes that user is root since it is only
427 called from the menu item which is invisible to
430 cmdPath = os.path.join(os.path.dirname(sys.argv[0]), \
431 "time-slider/plugins/rsync/rsync-backup")
432 if os.geteuid() == 0:
434 "%s:rsync" % (plugin.PLUGINBASEFMRI)]
436 cmd = ['/usr/bin/gksu' ,cmdPath, \
437 "%s:rsync" % (plugin.PLUGINBASEFMRI)]
439 subprocess.Popen(cmd, close_fds=True, cwd="/")
442 class CleanupNote(Note):
444 def __init__(self, icon, menu):
445 Note.__init__(self, icon, menu)
446 self._cleanupHead = None
447 self._cleanupBody = None
448 dbus.bus.NameOwnerWatch(bus,
449 "org.opensolaris.TimeSlider",
452 def _show_cleanup_details(self, *args):
453 # We could keep a dialog around but this a rare
454 # enough event that's it not worth the effort.
455 dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING,
456 buttons=gtk.BUTTONS_CLOSE)
457 dialog.set_title(_("Time Slider: Low Space Warning"))
458 dialog.set_markup("<b>%s</b>" % (self._cleanupHead))
459 dialog.format_secondary_markup(self._cleanupBody)
462 dialog.connect("response", self._dialog_response)
464 def _cleanup_handler(self, pool, severity, threshhold, sender=None, interface=None, path=None):
466 expiry = pynotify.EXPIRES_NEVER
467 urgency = pynotify.URGENCY_CRITICAL
468 self._cleanupHead = _("Emergency: \'%s\' is full!") % pool
469 notifyBody = _("The file system: \'%s\', is over %s%% full.") \
471 self._cleanupBody = _("The file system: \'%s\', is over %s%% full.\n"
472 "As an emergency measure, Time Slider has "
473 "destroyed all of its backups.\nTo fix this problem, "
474 "delete any unnecessary files on \'%s\', or add "
475 "disk space (see ZFS documentation).") \
476 % (pool, threshhold, pool)
478 expiry = pynotify.EXPIRES_NEVER
479 urgency = pynotify.URGENCY_CRITICAL
480 self._cleanupHead = _("Emergency: \'%s\' is almost full!") % pool
481 notifyBody = _("The file system: \'%s\', exceeded %s%% "
482 "of its total capacity") \
484 self._cleanupBody = _("The file system: \'%s\', exceeded %s%% "
485 "of its total capacity. As an emerency measure, "
486 "Time Slider has has destroyed most or all of its "
487 "backups to prevent the disk becoming full. "
488 "To prevent this from happening again, delete "
489 "any unnecessary files on \'%s\', or add disk "
490 "space (see ZFS documentation).") \
491 % (pool, threshhold, pool)
493 expiry = pynotify.EXPIRES_NEVER
494 urgency = pynotify.URGENCY_CRITICAL
495 self._cleanupHead = _("Urgent: \'%s\' is almost full!") % pool
496 notifyBody = _("The file system: \'%s\', exceeded %s%% "
497 "of its total capacity") \
499 self._cleanupBody = _("The file system: \'%s\', exceeded %s%% "
500 "of its total capacity. As a remedial measure, "
501 "Time Slider has destroyed some backups, and will "
502 "destroy more, eventually all, as capacity continues "
503 "to diminish.\nTo prevent this from happening again, "
504 "delete any unnecessary files on \'%s\', or add disk "
505 "space (see ZFS documentation).") \
506 % (pool, threshhold, pool)
508 expiry = 20000 # 20 seconds
509 urgency = pynotify.URGENCY_NORMAL
510 self._cleanupHead = _("Warning: \'%s\' is getting full") % pool
511 notifyBody = _("The file system: \'%s\', exceeded %s%% "
512 "of its total capacity") \
514 self._cleanupBody = _("\'%s\' exceeded %s%% of its total "
515 "capacity. To fix this, Time Slider has destroyed "
516 "some recent backups, and will destroy more as "
517 "capacity continues to diminish.\nTo prevent "
518 "this from happening again, delete any "
519 "unnecessary files on \'%s\', or add disk space "
520 "(see ZFS documentation).\n") \
521 % (pool, threshhold, pool)
523 return # No other values currently supported
525 if (self._note != None):
527 self._note = pynotify.Notification(self._cleanupHead,
529 self._note.add_action("clicked",
531 self._show_cleanup_details)
532 self._note.connect("closed",
533 self._notification_closed)
534 self._note.set_urgency(urgency)
535 self._note.set_timeout(expiry)
536 self._setup_icon_for_note()
537 self._icon.set_blinking(True)
538 gobject.idle_add(self._show_notification)
540 def _connect_to_object(self):
542 remote_object = bus.get_object("org.opensolaris.TimeSlider",
543 "/org/opensolaris/TimeSlider/autosnap")
544 except dbus.DBusException:
545 sys.stderr.write("Failed to connect to remote D-Bus object: " + \
546 "/org/opensolaris/TimeSlider/autosnap")
548 #Create an Interface wrapper for the remote object
549 iface = dbus.Interface(remote_object, "org.opensolaris.TimeSlider.autosnap")
551 iface.connect_to_signal("capacity_exceeded", self._cleanup_handler, sender_keyword='sender',
552 interface_keyword='interface', path_keyword='path')
556 class SetupNote(Note):
558 def __init__(self, icon, menu, manager):
559 Note.__init__(self, icon, menu)
560 # We are passed a reference to out parent so we can
561 # provide it notification which it can then circulate
562 # to other notification objects such as Rsync and
564 self._manager = manager
567 self._configSvcItem = gtk.MenuItem(_("Configure Time Slider..."))
568 self._configSvcItem.connect("activate",
569 self._run_config_app)
570 self._configSvcItem.set_sensitive(True)
571 self._menu.append(self._configSvcItem)
572 self._configSvcItem.show()
573 dbus.bus.NameOwnerWatch(bus,
574 "org.opensolaris.TimeSlider.config",
577 def _connect_to_object(self):
579 remote_object = bus.get_object("org.opensolaris.TimeSlider.config",
580 "/org/opensolaris/TimeSlider/config")
581 except dbus.DBusException:
582 sys.stderr.write("Failed to connect to remote D-Bus object: " + \
583 "/org/opensolaris/TimeSlider/config")
585 #Create an Interface wrapper for the remote object
586 iface = dbus.Interface(remote_object, "org.opensolaris.TimeSlider.config")
588 iface.connect_to_signal("config_changed", self._config_handler, sender_keyword='sender',
589 interface_keyword='interface', path_keyword='path')
591 def _config_handler(self, sender=None, interface=None, path=None):
592 # Notify the manager.
593 # This will eventually propogate through to an invocation
594 # of our own refresh() method.
595 self._manager.refresh()
597 def _run_config_app(self, menuItem):
598 cmdPath = os.path.join(os.path.dirname(sys.argv[0]),
602 cmd = os.path.abspath(cmdPath)
603 # The setup GUI deals with it's own security and
604 # authorisation, so no need to pfexec it. Any
605 # changes made to configuration will come back to
606 # us by way of D-Bus notification.
607 subprocess.Popen(cmd, close_fds=True)
611 # Notification objects need to share a common
612 # status icon and popup menu so these are created
613 # outside the object and passed to the constructor
614 self._menu = gtk.Menu()
615 self._icon = gtk.StatusIcon()
616 self._icon.set_from_icon_name("time-slider-setup")
617 self._setupNote = SetupNote(self._icon,
620 self._cleanupNote = CleanupNote(self._icon,
622 self._rsyncNote = RsyncNote(self._icon,
626 self._rsyncNote.refresh()
628 bus = dbus.SystemBus()
631 mainloop = gobject.MainLoop()
632 dbus.mainloop.glib.DBusGMainLoop(set_as_default = True)
633 gobject.threads_init()
634 pynotify.init(_("Time Slider"))
636 noteManager = NoteManager()
643 if __name__ == '__main__':