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]
31 from bisect import insort
41 gtk.gdk.threads_init()
50 from os.path import abspath, dirname, join, pardir
51 sys.path.insert(0, join(dirname(__file__), pardir, "plugin"))
53 sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync"))
57 # here we define the path constants so that other modules can use it.
58 # this allows us to get access to the shared files without having to
59 # know the actual location, we just use the location of the current
60 # file and use paths relative to that.
61 SHARED_FILES = os.path.abspath(os.path.join(os.path.dirname(__file__),
64 LOCALE_PATH = os.path.join('/usr', 'share', 'locale')
65 RESOURCE_PATH = os.path.join(SHARED_FILES, 'res')
67 # the name of the gettext domain. because we have our translation files
68 # not in a global folder this doesn't really matter, setting it to the
69 # application name is a good idea tough.
70 GETTEXT_DOMAIN = 'time-slider'
72 # set up the glade gettext system and locales
73 gtk.glade.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH)
74 gtk.glade.textdomain(GETTEXT_DOMAIN)
77 from rbac import RBACprofile
81 def __init__(self, mountpoint, rsync_dir = None, fsname= None, snaplabel= None, creationtime= None):
84 self.__init_from_mp (mountpoint)
86 self.rsync_dir = rsync_dir
87 self.mountpoint = mountpoint
89 self.snaplabel = snaplabel
91 self.creationtime = creationtime
93 tm = time.localtime(self.creationtime)
94 self.creationtime_str = unicode(time.strftime ("%c", tm),
95 locale.getpreferredencoding()).encode('utf-8')
97 self.creationtime_str = time.ctime(self.creationtime)
98 fs = zfs.Filesystem (self.fsname)
99 self.zfs_mountpoint = fs.get_mountpoint ()
101 def __init_from_mp (self, mountpoint):
102 self.rsyncsmf = rsyncsmf.RsyncSMF("%s:rsync" %(plugin.PLUGINBASEFMRI))
103 rsyncBaseDir = self.rsyncsmf.get_target_dir()
104 sys,nodeName,rel,ver,arch = os.uname()
105 self.rsync_dir = os.path.join(rsyncBaseDir,
106 rsyncsmf.RSYNCDIRPREFIX,
108 self.mountpoint = mountpoint
110 s1 = mountpoint.split ("%s/" % self.rsync_dir, 1)
111 s2 = s1[1].split ("/%s" % rsyncsmf.RSYNCDIRSUFFIX, 1)
112 s3 = s2[1].split ('/',2)
114 self.snaplabel = s3[1]
115 self.creationtime = os.stat(mountpoint).st_mtime
118 ret = "self.rsync_dir = %s\n \
119 self.mountpoint = %s\n \
121 self.snaplabel = %s\n" % (self.rsync_dir,
122 self.mountpoint, self.fsname,
128 return os.path.exists(self.mountpoint)
131 lockFileDir = os.path.join(self.rsync_dir,
133 rsyncsmf.RSYNCLOCKSUFFIX)
135 if not os.path.exists(lockFileDir):
136 os.makedirs(lockFileDir, 0755)
138 lockFile = os.path.join(lockFileDir, self.snaplabel + ".lock")
140 lockFp = open(lockFile, 'w')
141 fcntl.flock(lockFp, fcntl.LOCK_EX | fcntl.LOCK_NB)
143 raise RuntimeError, \
144 "couldn't delete %s, already used by another process" % self.mountpoint
147 trashDir = os.path.join(self.rsync_dir,
149 rsyncsmf.RSYNCTRASHSUFFIX)
150 if not os.path.exists(trashDir):
151 os.makedirs(trashDir, 0755)
153 backupTrashDir = os.path.join (self.rsync_dir,
155 rsyncsmf.RSYNCTRASHSUFFIX,
159 os.rename (self.mountpoint, backupTrashDir)
160 shutil.rmtree (backupTrashDir)
162 log = "%s/%s/%s/%s.log" % (self.rsync_dir,
164 rsyncsmf.RSYNCLOGSUFFIX,
166 if os.path.exists (log):
172 class DeleteSnapManager:
174 def __init__(self, snapshots = None):
175 self.xml = gtk.glade.XML("%s/../../glade/time-slider-delete.glade" \
176 % (os.path.dirname(__file__)))
177 self.backuptodelete = []
178 self.shortcircuit = []
179 maindialog = self.xml.get_widget("time-slider-delete")
180 self.pulsedialog = self.xml.get_widget("pulsedialog")
181 self.pulsedialog.set_transient_for(maindialog)
182 self.datasets = zfs.Datasets()
185 self.shortcircuit = snapshots
187 glib.idle_add(self.__init_scan)
189 self.progressdialog = self.xml.get_widget("deletingdialog")
190 self.progressdialog.set_transient_for(maindialog)
191 self.progressbar = self.xml.get_widget("deletingprogress")
193 dic = {"on_closebutton_clicked" : gtk.main_quit,
194 "on_window_delete_event" : gtk.main_quit,
195 "on_snapshotmanager_delete_event" : gtk.main_quit,
196 "on_fsfilterentry_changed" : self.__on_filterentry_changed,
197 "on_schedfilterentry_changed" : self.__on_filterentry_changed,
198 "on_typefiltercombo_changed" : self.__on_filterentry_changed,
199 "on_selectbutton_clicked" : self.__on_selectbutton_clicked,
200 "on_deselectbutton_clicked" : self.__on_deselectbutton_clicked,
201 "on_deletebutton_clicked" : self.__on_deletebutton_clicked,
202 "on_confirmcancel_clicked" : self.__on_confirmcancel_clicked,
203 "on_confirmdelete_clicked" : self.__on_confirmdelete_clicked,
204 "on_errordialog_response" : self.__on_errordialog_response}
205 self.xml.signal_autoconnect(dic)
207 def initialise_view(self):
208 if len(self.shortcircuit) == 0:
210 self.liststorefs = gtk.ListStore(str, str, str, str, str, long,
211 gobject.TYPE_PYOBJECT)
212 list_filter = self.liststorefs.filter_new()
213 list_sort = gtk.TreeModelSort(list_filter)
214 list_sort.set_sort_column_id(1, gtk.SORT_ASCENDING)
216 self.snaptreeview = self.xml.get_widget("snaplist")
217 self.snaptreeview.set_model(self.liststorefs)
218 self.snaptreeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
220 cell0 = gtk.CellRendererText()
221 cell1 = gtk.CellRendererText()
222 cell2 = gtk.CellRendererText()
223 cell3 = gtk.CellRendererText()
224 cell4 = gtk.CellRendererText()
225 cell5 = gtk.CellRendererText()
227 typecol = gtk.TreeViewColumn(_("Type"),
229 typecol.set_sort_column_id(0)
230 typecol.set_resizable(True)
231 typecol.connect("clicked",
232 self.__on_treeviewcol_clicked, 0)
233 self.snaptreeview.append_column(typecol)
235 mountptcol = gtk.TreeViewColumn(_("Mount Point"),
237 mountptcol.set_sort_column_id(1)
238 mountptcol.set_resizable(True)
239 mountptcol.connect("clicked",
240 self.__on_treeviewcol_clicked, 1)
241 self.snaptreeview.append_column(mountptcol)
243 fsnamecol = gtk.TreeViewColumn(_("File System Name"),
245 fsnamecol.set_sort_column_id(2)
246 fsnamecol.set_resizable(True)
247 fsnamecol.connect("clicked",
248 self.__on_treeviewcol_clicked, 2)
249 self.snaptreeview.append_column(fsnamecol)
251 snaplabelcol = gtk.TreeViewColumn(_("Snapshot Name"),
253 snaplabelcol.set_sort_column_id(3)
254 snaplabelcol.set_resizable(True)
255 snaplabelcol.connect("clicked",
256 self.__on_treeviewcol_clicked, 3)
257 self.snaptreeview.append_column(snaplabelcol)
259 cell4.props.xalign = 1.0
260 creationcol = gtk.TreeViewColumn(_("Creation Time"),
262 creationcol.set_sort_column_id(5)
263 creationcol.set_resizable(True)
264 creationcol.connect("clicked",
265 self.__on_treeviewcol_clicked, 5)
266 self.snaptreeview.append_column(creationcol)
268 # Note to developers.
269 # The second element is for internal matching and should not
270 # be i18ned under any circumstances.
271 typestore = gtk.ListStore(str, str)
272 typestore.append([_("All"), "All"])
273 typestore.append([_("Backups"), "Backup"])
274 typestore.append([_("Snapshots"), "Snapshot"])
276 self.typefiltercombo = self.xml.get_widget("typefiltercombo")
277 self.typefiltercombo.set_model(typestore)
278 typefiltercomboCell = gtk.CellRendererText()
279 self.typefiltercombo.pack_start(typefiltercomboCell, True)
280 self.typefiltercombo.add_attribute(typefiltercomboCell, 'text',0)
282 # Note to developers.
283 # The second element is for internal matching and should not
284 # be i18ned under any circumstances.
285 fsstore = gtk.ListStore(str, str)
286 fslist = self.datasets.list_filesystems()
287 fsstore.append([_("All"), None])
288 for fsname,fsmount in fslist:
289 fsstore.append([fsname, fsname])
290 self.fsfilterentry = self.xml.get_widget("fsfilterentry")
291 self.fsfilterentry.set_model(fsstore)
292 self.fsfilterentry.set_text_column(0)
293 fsfilterentryCell = gtk.CellRendererText()
294 self.fsfilterentry.pack_start(fsfilterentryCell)
296 schedstore = gtk.ListStore(str, str)
297 # Note to developers.
298 # The second element is for internal matching and should not
299 # be i18ned under any circumstances.
300 schedstore.append([_("All"), None])
301 schedstore.append([_("Monthly"), "monthly"])
302 schedstore.append([_("Weekly"), "weekly"])
303 schedstore.append([_("Daily"), "daily"])
304 schedstore.append([_("Hourly"), "hourly"])
305 schedstore.append([_("1/4 Hourly"), "frequent"])
306 self.schedfilterentry = self.xml.get_widget("schedfilterentry")
307 self.schedfilterentry.set_model(schedstore)
308 self.schedfilterentry.set_text_column(0)
309 schedentryCell = gtk.CellRendererText()
310 self.schedfilterentry.pack_start(schedentryCell)
312 self.schedfilterentry.set_active(0)
313 self.fsfilterentry.set_active(0)
314 self.typefiltercombo.set_active(0)
316 cloned = self.datasets.list_cloned_snapshots()
319 for snapname in self.shortcircuit:
320 # Filter out snapshots that are the root
321 # of cloned filesystems or volumes
323 cloned.index(snapname)
324 dialog = gtk.MessageDialog(None,
328 _("Snapshot can not be deleted"))
329 text = _("%s has one or more dependent clones "
330 "and will not be deleted. To delete "
331 "this snapshot, first delete all "
332 "datasets and snapshots cloned from "
335 dialog.format_secondary_text(text)
339 path = os.path.abspath (snapname)
340 if not os.path.exists (path):
341 snapshot = zfs.Snapshot(snapname)
342 self.backuptodelete.append(snapshot)
345 self.backuptodelete.append(RsyncBackup (snapname))
348 confirm = self.xml.get_widget("confirmdialog")
349 summary = self.xml.get_widget("summarylabel")
350 total = len(self.backuptodelete)
355 text = _("1 external backup will be deleted.")
357 text = _("%d external backups will be deleted.") % num_rsync
363 text += _("1 snapshot will be deleted.")
365 text += _("%d snapshots will be deleted.") % num_snap
367 summary.set_text(text )
368 response = confirm.run()
372 # Create the thread in an idle loop in order to
373 # avoid deadlock inside gtk.
374 glib.idle_add(self.__init_delete)
377 def __on_treeviewcol_clicked(self, widget, searchcol):
378 self.snaptreeview.set_search_column(searchcol)
380 def __filter_snapshot_list(self, list, filesys = None, snap = None, btype = None):
381 if filesys == None and snap == None and btype == None:
385 for snapshot in list:
386 if snapshot.fsname.find(filesys) != -1:
387 fssublist.append(snapshot)
393 for snapshot in fssublist:
394 if snapshot.snaplabel.find(snap) != -1:
395 snaplist.append(snapshot)
400 if btype != None and btype != "All":
401 for item in snaplist:
402 if btype == "Backup":
403 if isinstance(item, RsyncBackup):
404 typelist.append (item)
406 if isinstance(item, zfs.Snapshot):
407 typelist.append (item)
413 def __on_filterentry_changed(self, widget):
414 # Get the filesystem filter value
415 iter = self.fsfilterentry.get_active_iter()
417 filesys = self.fsfilterentry.get_active_text()
419 model = self.fsfilterentry.get_model()
420 filesys = model.get(iter, 1)[0]
421 # Get the snapshot name filter value
422 iter = self.schedfilterentry.get_active_iter()
424 snap = self.schedfilterentry.get_active_text()
426 model = self.schedfilterentry.get_model()
427 snap = model.get(iter, 1)[0]
429 # Get the type filter value
430 iter = self.typefiltercombo.get_active_iter()
434 model = self.typefiltercombo.get_model()
435 type = model.get(iter, 1)[0]
437 self.liststorefs.clear()
438 newlist = self.__filter_snapshot_list(self.snapscanner.snapshots,
441 for snapshot in newlist:
443 tm = time.localtime(snapshot.get_creation_time())
444 t = unicode(time.strftime ("%c", tm),
445 locale.getpreferredencoding()).encode('utf-8')
447 t = time.ctime(snapshot.get_creation_time())
449 mount_point = self.snapscanner.mounts[snapshot.fsname]
450 if (mount_point == "legacy"):
451 mount_point = _("Legacy")
453 self.liststorefs.append([
459 snapshot.get_creation_time(),
463 # This will catch exceptions from things we ignore
464 # such as dump as swap volumes and skip over them.
466 newlist = self.__filter_snapshot_list(self.snapscanner.rsynced_backups,
469 for backup in newlist:
470 self.liststorefs.append([_("Backup"),
471 backup.zfs_mountpoint,
474 backup.creationtime_str,
478 def __on_selectbutton_clicked(self, widget):
479 selection = self.snaptreeview.get_selection()
480 selection.select_all()
483 def __on_deselectbutton_clicked(self, widget):
484 selection = self.snaptreeview.get_selection()
485 selection.unselect_all()
488 def __on_deletebutton_clicked(self, widget):
489 self.backuptodelete = []
490 selection = self.snaptreeview.get_selection()
491 selection.selected_foreach(self.__add_selection)
492 total = len(self.backuptodelete)
496 confirm = self.xml.get_widget("confirmdialog")
497 summary = self.xml.get_widget("summarylabel")
501 for item in self.backuptodelete:
502 if isinstance (item, RsyncBackup):
510 str = _("1 external backup will be deleted.")
512 str = _("%d external backups will be deleted.") % num_rsync
518 str += _("1 snapshot will be deleted.")
520 str += _("%d snapshots will be deleted.") % num_snap
522 summary.set_text(str)
523 response = confirm.run()
527 glib.idle_add(self.__init_delete)
530 def __init_scan(self):
531 self.snapscanner = ScanSnapshots()
532 self.pulsedialog.show()
533 self.snapscanner.start()
534 glib.timeout_add(100, self.__monitor_scan)
537 def __init_delete(self):
538 self.snapdeleter = DeleteSnapshots(self.backuptodelete)
539 # If there's more than a few snapshots, pop up
541 if len(self.backuptodelete) > 3:
542 self.progressbar.set_fraction(0.0)
543 self.progressdialog.show()
544 self.snapdeleter.start()
545 glib.timeout_add(300, self.__monitor_deletion)
548 def __monitor_scan(self):
549 if self.snapscanner.isAlive() == True:
550 self.xml.get_widget("pulsebar").pulse()
553 self.pulsedialog.hide()
554 if self.snapscanner.errors:
556 dialog = gtk.MessageDialog(None,
560 _("Some snapshots could not be read"))
561 dialog.connect("response",
562 self.__on_errordialog_response)
563 for error in self.snapscanner.errors:
564 details = details + error
565 dialog.format_secondary_text(details)
567 self.__on_filterentry_changed(None)
570 def __monitor_deletion(self):
571 if self.snapdeleter.isAlive() == True:
572 self.progressbar.set_fraction(self.snapdeleter.progress)
575 self.progressdialog.hide()
576 self.progressbar.set_fraction(1.0)
577 self.progressdialog.hide()
578 if self.snapdeleter.errors:
580 dialog = gtk.MessageDialog(None,
584 _("Some snapshots could not be deleted"))
585 dialog.connect("response",
586 self.__on_errordialog_response)
587 for error in self.snapdeleter.errors:
588 details = details + error
589 dialog.format_secondary_text(details)
591 # If we didn't shortcircut straight to the delete confirmation
592 # dialog then the main dialog is visible so we rebuild the list
594 if len(self.shortcircuit) == 0:
595 self.__refresh_view()
600 def __refresh_view(self):
601 self.liststorefs.clear()
602 glib.idle_add(self.__init_scan)
603 self.backuptodelete = []
605 def __add_selection(self, treemodel, path, iter):
606 snapshot = treemodel.get(iter, 8)[0]
607 self.backuptodelete.append(snapshot)
609 def __on_confirmcancel_clicked(self, widget):
610 widget.get_toplevel().hide()
611 widget.get_toplevel().response(1)
613 def __on_confirmdelete_clicked(self, widget):
614 widget.get_toplevel().hide()
615 widget.get_toplevel().response(2)
617 def __on_errordialog_response(self, widget, responseid):
620 class ScanSnapshots(threading.Thread):
623 threading.Thread.__init__(self)
625 self.datasets = zfs.Datasets()
628 self.rsynced_backups = []
631 self.mounts = self.__get_fs_mountpoints()
632 self.rsyncsmf = rsyncsmf.RsyncSMF("%s:rsync" %(plugin.PLUGINBASEFMRI))
633 self.__get_rsync_backups ()
636 def __get_rsync_backups (self):
637 # get rsync backup dir
638 self.rsyncsmf = rsyncsmf.RsyncSMF("%s:rsync" %(plugin.PLUGINBASEFMRI))
639 rsyncBaseDir = self.rsyncsmf.get_target_dir()
640 sys,nodeName,rel,ver,arch = os.uname()
641 self.rsyncDir = os.path.join(rsyncBaseDir,
642 rsyncsmf.RSYNCDIRPREFIX,
644 if not os.path.exists(self.rsyncDir):
649 for root, dirs, files in os.walk(self.rsyncDir):
650 if '.time-slider' in dirs:
651 dirs.remove('.time-slider')
652 backupDir = os.path.join(root, rsyncsmf.RSYNCDIRSUFFIX)
653 if os.path.exists(backupDir):
654 insort(rootBackupDirs, os.path.abspath(backupDir))
656 for dirName in rootBackupDirs:
658 for d in os.listdir(dirName):
659 if os.path.isdir(d) and not os.path.islink(d):
660 s1 = dirName.split ("%s/" % self.rsyncDir, 1)
661 s2 = s1[1].split ("/%s" % rsyncsmf.RSYNCDIRSUFFIX, 1)
664 rb = RsyncBackup ("%s/%s" %(dirName, d),
669 self.rsynced_backups.append (rb)
671 def __get_fs_mountpoints(self):
672 """Returns a dictionary mapping:
673 {filesystem : mountpoint}"""
675 for filesys,mountpoint in self.datasets.list_filesystems():
676 result[filesys] = mountpoint
680 cloned = self.datasets.list_cloned_snapshots()
682 snaplist = self.datasets.list_snapshots()
683 for snapname,snaptime in snaplist:
684 # Filter out snapshots that are the root
685 # of cloned filesystems or volumes
687 cloned.index(snapname)
689 snapshot = zfs.Snapshot(snapname, snaptime)
690 self.snapshots.append(snapshot)
692 class DeleteSnapshots(threading.Thread):
694 def __init__(self, snapshots):
695 threading.Thread.__init__(self)
696 self.backuptodelete = snapshots
698 self.completed = False
705 total = len(self.backuptodelete)
706 for backup in self.backuptodelete:
707 # The backup could have expired and been automatically
708 # destroyed since the user selected it. Check that it
709 # still exists before attempting to delete it. If it
710 # doesn't exist just silently ignore it.
714 except RuntimeError, inst:
715 self.errors.append(str(inst))
717 self.progress = deleted / (total * 1.0)
718 self.completed = True
722 opts,args = getopt.getopt(sys.argv[1:], "", [])
723 except getopt.GetoptError:
725 rbacp = RBACprofile()
726 if os.geteuid() == 0:
728 manager = DeleteSnapManager(args)
730 manager = DeleteSnapManager()
731 gtk.gdk.threads_enter()
732 glib.idle_add(manager.initialise_view)
734 gtk.gdk.threads_leave()
735 elif rbacp.has_profile("Primary Administrator") or \
736 rbacp.has_profile("ZFS File System Management"):
737 # Run via pfexec, which will launch the GUI as superuser
739 arguments.append ("pfexec")
740 arguments.append (argv)
742 os.execv("/usr/bin/pfexec", arguments)
743 # Shouldn't reach this point
745 elif os.path.exists(argv) and os.path.exists("/usr/bin/gksu"):
746 # Run via gksu, which will prompt for the root password
747 newargs = ["gksu", argv]
750 os.execv("/usr/bin/gksu", newargs);
751 # Shouldn't reach this point
754 dialog = gtk.MessageDialog(None,
758 _("Insufficient Priviliges"))
759 dialog.format_secondary_text(_("Snapshot deletion requires "
760 "administrative privileges to run. "
761 "You have not been assigned the necessary"
762 "administrative priviliges."
763 "\n\nConsult your system administrator "))
765 print argv + "is not a valid executable path"