f619b3e40d30ddb836be58bb99052875cc488930
[time-slider.git] / usr / share / time-slider / lib / time_slider / deletegui.py
1 #!/usr/bin/python2
2 #
3 # CDDL HEADER START
4 #
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.
8 #
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.
13 #
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]
19 #
20 # CDDL HEADER END
21 #
22
23 import threading
24 import sys
25 import os
26 import time
27 import getopt
28 import locale
29 import shutil
30 import fcntl
31 from bisect import insort
32
33 try:
34     import pygtk
35     pygtk.require("2.4")
36 except:
37     pass
38 try:
39     import gtk
40     import gtk.glade
41     gtk.gdk.threads_init()
42 except:
43     sys.exit(1)
44 try:
45     import glib
46     import gobject
47 except:
48     sys.exit(1)
49
50 from os.path import abspath, dirname, join, pardir
51 sys.path.insert(0, join(dirname(__file__), pardir, "plugin"))
52 import plugin
53 sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync"))
54 import rsyncsmf
55
56
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__),
62                                os.path.pardir,
63                                os.path.pardir))
64 LOCALE_PATH = os.path.join('/usr', 'share', 'locale')
65 RESOURCE_PATH = os.path.join(SHARED_FILES, 'res')
66
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'
71
72 # set up the glade gettext system and locales
73 gtk.glade.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH)
74 gtk.glade.textdomain(GETTEXT_DOMAIN)
75
76 import zfs
77 from rbac import RBACprofile
78
79 class RsyncBackup:
80
81     def __init__(self, mountpoint, rsync_dir = None,  fsname= None, snaplabel= None, creationtime= None):
82
83         if rsync_dir == None:
84             self.__init_from_mp (mountpoint)
85         else:
86             self.rsync_dir = rsync_dir
87             self.mountpoint = mountpoint
88             self.fsname = fsname
89             self.snaplabel = snaplabel
90
91             self.creationtime = creationtime
92             try:
93                 tm = time.localtime(self.creationtime)
94                 self.creationtime_str = unicode(time.strftime ("%c", tm),
95                            locale.getpreferredencoding()).encode('utf-8')
96             except:
97                 self.creationtime_str = time.ctime(self.creationtime)
98         fs = zfs.Filesystem (self.fsname)
99         self.zfs_mountpoint = fs.get_mountpoint ()
100
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,
107                                      nodeName)
108         self.mountpoint = mountpoint
109
110         s1 = mountpoint.split ("%s/" % self.rsync_dir, 1)
111         s2 = s1[1].split ("/%s" % rsyncsmf.RSYNCDIRSUFFIX, 1)
112         s3 = s2[1].split ('/',2)
113         self.fsname = s2[0]
114         self.snaplabel =  s3[1]
115         self.creationtime = os.stat(mountpoint).st_mtime
116
117     def __str__(self):
118         ret = "self.rsync_dir = %s\n \
119                self.mountpoint = %s\n \
120                self.fsname = %s\n \
121                self.snaplabel = %s\n" % (self.rsync_dir,
122                                          self.mountpoint, self.fsname,
123                                          self.snaplabel)
124         return ret
125
126
127     def exists(self):
128         return os.path.exists(self.mountpoint)
129
130     def destroy(self):
131         lockFileDir = os.path.join(self.rsync_dir,
132                              self.fsname,
133                              rsyncsmf.RSYNCLOCKSUFFIX)
134
135         if not os.path.exists(lockFileDir):
136             os.makedirs(lockFileDir, 0755)
137
138         lockFile = os.path.join(lockFileDir, self.snaplabel + ".lock")
139         try:
140             lockFp = open(lockFile, 'w')
141             fcntl.flock(lockFp, fcntl.LOCK_EX | fcntl.LOCK_NB)
142         except IOError:
143             raise RuntimeError, \
144             "couldn't delete %s, already used by another process" % self.mountpoint
145             return
146
147         trashDir = os.path.join(self.rsync_dir,
148                           self.fsname,
149                           rsyncsmf.RSYNCTRASHSUFFIX)
150         if not os.path.exists(trashDir):
151             os.makedirs(trashDir, 0755)
152
153         backupTrashDir = os.path.join (self.rsync_dir,
154                                  self.fsname,
155                                  rsyncsmf.RSYNCTRASHSUFFIX,
156                                  self.snaplabel)
157
158         # move then delete
159         os.rename (self.mountpoint, backupTrashDir)
160         shutil.rmtree (backupTrashDir)
161
162         log = "%s/%s/%s/%s.log" % (self.rsync_dir,
163                                    self.fsname,
164                                    rsyncsmf.RSYNCLOGSUFFIX,
165                                    self.snaplabel)
166         if os.path.exists (log):
167             os.unlink (log)
168
169         lockFp.close()
170         os.unlink(lockFile)
171
172 class DeleteSnapManager:
173
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()
183         if snapshots:
184             maindialog.hide()
185             self.shortcircuit = snapshots
186         else:
187             glib.idle_add(self.__init_scan)
188
189         self.progressdialog = self.xml.get_widget("deletingdialog")
190         self.progressdialog.set_transient_for(maindialog)
191         self.progressbar = self.xml.get_widget("deletingprogress")
192         # signal dictionary
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)
206
207     def initialise_view(self):
208         if len(self.shortcircuit) == 0:
209             # Set TreeViews
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)
215
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)
219
220             cell0 = gtk.CellRendererText()
221             cell1 = gtk.CellRendererText()
222             cell2 = gtk.CellRendererText()
223             cell3 = gtk.CellRendererText()
224             cell4 = gtk.CellRendererText()
225             cell5 = gtk.CellRendererText()
226
227             typecol = gtk.TreeViewColumn(_("Type"),
228                                             cell0, text = 0)
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)
234
235             mountptcol = gtk.TreeViewColumn(_("Mount Point"),
236                                             cell1, text = 1)
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)
242
243             fsnamecol = gtk.TreeViewColumn(_("File System Name"),
244                                            cell2, text = 2)
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)
250
251             snaplabelcol = gtk.TreeViewColumn(_("Snapshot Name"),
252                                               cell3, text = 3)
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)
258
259             cell4.props.xalign = 1.0
260             creationcol = gtk.TreeViewColumn(_("Creation Time"),
261                                              cell4, text = 4)
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)
267
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"])
275
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)
281
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)
295
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)
311
312             self.schedfilterentry.set_active(0)
313             self.fsfilterentry.set_active(0)
314             self.typefiltercombo.set_active(0)
315         else:
316             cloned = self.datasets.list_cloned_snapshots()
317             num_snap = 0
318             num_rsync = 0
319             for snapname in self.shortcircuit:
320                 # Filter out snapshots that are the root
321                 # of cloned filesystems or volumes
322                 try:
323                     cloned.index(snapname)
324                     dialog = gtk.MessageDialog(None,
325                                    0,
326                                    gtk.MESSAGE_ERROR,
327                                    gtk.BUTTONS_CLOSE,
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 "
333                              "this snapshot.") \
334                              % snapname
335                     dialog.format_secondary_text(text)
336                     dialog.run()
337                     sys.exit(1)
338                 except ValueError:
339                     path = os.path.abspath (snapname)
340                     if not os.path.exists (path):
341                         snapshot = zfs.Snapshot(snapname)
342                         self.backuptodelete.append(snapshot)
343                         num_snap += 1
344                     else:
345                         self.backuptodelete.append(RsyncBackup (snapname))
346                         num_rsync += 1
347
348             confirm = self.xml.get_widget("confirmdialog")
349             summary = self.xml.get_widget("summarylabel")
350             total = len(self.backuptodelete)
351
352             text = ""
353             if num_rsync != 0 :
354                 if num_rsync == 1:
355                     text = _("1 external backup will be deleted.")
356                 else:
357                     text = _("%d external backups will be deleted.") % num_rsync
358
359             if num_snap != 0 :
360                 if len(text) != 0:
361                     text += "\n"
362                 if num_snap == 1:
363                     text += _("1 snapshot will be deleted.")
364                 else:
365                     text += _("%d snapshots will be deleted.") % num_snap
366
367             summary.set_text(text )
368             response = confirm.run()
369             if response != 2:
370                 sys.exit(0)
371             else:
372                 # Create the thread in an idle loop in order to
373                 # avoid deadlock inside gtk.
374                 glib.idle_add(self.__init_delete)
375         return False
376
377     def __on_treeviewcol_clicked(self, widget, searchcol):
378         self.snaptreeview.set_search_column(searchcol)
379
380     def __filter_snapshot_list(self, list, filesys = None, snap = None, btype = None):
381         if filesys == None and snap == None and btype == None:
382             return list
383         fssublist = []
384         if filesys != None:
385             for snapshot in list:
386                 if snapshot.fsname.find(filesys) != -1:
387                     fssublist.append(snapshot)
388         else:
389             fssublist = list
390
391         snaplist = []
392         if snap != None:
393             for snapshot in fssublist:
394                 if  snapshot.snaplabel.find(snap) != -1:
395                     snaplist.append(snapshot)
396         else:
397             snaplist = fssublist
398
399         typelist = []
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)
405                 else:
406                     if isinstance(item, zfs.Snapshot):
407                         typelist.append (item)
408         else:
409             typelist = snaplist
410
411         return typelist
412
413     def __on_filterentry_changed(self, widget):
414         # Get the filesystem filter value
415         iter = self.fsfilterentry.get_active_iter()
416         if iter == None:
417             filesys = self.fsfilterentry.get_active_text()
418         else:
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()
423         if iter == None:
424             snap = self.schedfilterentry.get_active_text()
425         else:
426             model = self.schedfilterentry.get_model()
427             snap = model.get(iter, 1)[0]
428
429         # Get the type filter value
430         iter = self.typefiltercombo.get_active_iter()
431         if iter == None:
432             type = "All"
433         else:
434             model = self.typefiltercombo.get_model()
435             type = model.get(iter, 1)[0]
436
437         self.liststorefs.clear()
438         newlist = self.__filter_snapshot_list(self.snapscanner.snapshots,
439                     filesys,
440                     snap, type)
441         for snapshot in newlist:
442             try:
443                 tm = time.localtime(snapshot.get_creation_time())
444                 t = unicode(time.strftime ("%c", tm),
445                     locale.getpreferredencoding()).encode('utf-8')
446             except:
447                 t = time.ctime(snapshot.get_creation_time())
448             try:
449                 mount_point = self.snapscanner.mounts[snapshot.fsname]
450                 if (mount_point == "legacy"):
451                     mount_point = _("Legacy")
452
453                 self.liststorefs.append([
454                        _("Snapshot"),
455                        mount_point,
456                        snapshot.fsname,
457                        snapshot.snaplabel,
458                        t,
459                        snapshot.get_creation_time(),
460                        snapshot])
461             except KeyError:
462                 continue
463                 # This will catch exceptions from things we ignore
464                 # such as dump as swap volumes and skip over them.
465             # add rsync backups
466         newlist = self.__filter_snapshot_list(self.snapscanner.rsynced_backups,
467                                                 filesys,
468                                                 snap, type)
469         for backup in newlist:
470             self.liststorefs.append([_("Backup"),
471                                      backup.zfs_mountpoint,
472                                      backup.fsname,
473                                      backup.snaplabel,
474                                      backup.creationtime_str,
475                                      backup.creationtime,
476                                      backup])
477
478     def __on_selectbutton_clicked(self, widget):
479         selection = self.snaptreeview.get_selection()
480         selection.select_all()
481         return
482
483     def __on_deselectbutton_clicked(self, widget):
484         selection = self.snaptreeview.get_selection()
485         selection.unselect_all()
486         return
487
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)
493         if total <= 0:
494             return
495
496         confirm = self.xml.get_widget("confirmdialog")
497         summary = self.xml.get_widget("summarylabel")
498
499         num_snap = 0
500         num_rsync = 0
501         for item in self.backuptodelete:
502             if isinstance (item, RsyncBackup):
503                 num_rsync+=1
504             else:
505                 num_snap+=1
506
507         str = ""
508         if num_rsync != 0 :
509             if num_rsync == 1:
510                 str = _("1 external backup will be deleted.")
511             else:
512                 str = _("%d external backups will be deleted.") % num_rsync
513
514         if num_snap != 0 :
515             if len(str) != 0:
516                 str += "\n"
517             if num_snap == 1:
518                 str += _("1 snapshot will be deleted.")
519             else:
520                 str += _("%d snapshots will be deleted.") % num_snap
521
522         summary.set_text(str)
523         response = confirm.run()
524         if response != 2:
525             return
526         else:
527             glib.idle_add(self.__init_delete)
528         return
529
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)
535         return False
536
537     def __init_delete(self):
538         self.snapdeleter = DeleteSnapshots(self.backuptodelete)
539         # If there's more than a few snapshots, pop up
540         # a progress bar.
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)
546         return False
547
548     def __monitor_scan(self):
549         if self.snapscanner.isAlive() == True:
550             self.xml.get_widget("pulsebar").pulse()
551             return True
552         else:
553             self.pulsedialog.hide()
554             if self.snapscanner.errors:
555                 details = ""
556                 dialog = gtk.MessageDialog(None,
557                             0,
558                             gtk.MESSAGE_ERROR,
559                             gtk.BUTTONS_CLOSE,
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)
566                 dialog.show()
567             self.__on_filterentry_changed(None)
568             return False
569
570     def __monitor_deletion(self):
571         if self.snapdeleter.isAlive() == True:
572             self.progressbar.set_fraction(self.snapdeleter.progress)
573             return True
574         else:
575             self.progressdialog.hide()
576             self.progressbar.set_fraction(1.0)
577             self.progressdialog.hide()
578             if self.snapdeleter.errors:
579                 details = ""
580                 dialog = gtk.MessageDialog(None,
581                             0,
582                             gtk.MESSAGE_ERROR,
583                             gtk.BUTTONS_CLOSE,
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)
590                 dialog.show()
591             # If we didn't shortcircut straight to the delete confirmation
592             # dialog then the main dialog is visible so we rebuild the list
593             # view.
594             if len(self.shortcircuit) ==  0:
595                 self.__refresh_view()
596             else:
597                 gtk.main_quit()
598             return False
599
600     def __refresh_view(self):
601         self.liststorefs.clear()
602         glib.idle_add(self.__init_scan)
603         self.backuptodelete = []
604
605     def __add_selection(self, treemodel, path, iter):
606         snapshot = treemodel.get(iter, 6)[0]
607         self.backuptodelete.append(snapshot)
608
609     def __on_confirmcancel_clicked(self, widget):
610         widget.get_toplevel().hide()
611         widget.get_toplevel().response(1)
612
613     def __on_confirmdelete_clicked(self, widget):
614         widget.get_toplevel().hide()
615         widget.get_toplevel().response(2)
616
617     def __on_errordialog_response(self, widget, responseid):
618         widget.hide()
619
620 class ScanSnapshots(threading.Thread):
621
622     def __init__(self):
623         threading.Thread.__init__(self)
624         self.errors = []
625         self.datasets = zfs.Datasets()
626         self.snapshots = []
627         self.rsynced_fs = []
628         self.rsynced_backups = []
629
630     def run(self):
631         self.mounts = self.__get_fs_mountpoints()
632         self.rsyncsmf = rsyncsmf.RsyncSMF("%s:rsync" %(plugin.PLUGINBASEFMRI))
633         self.__get_rsync_backups ()
634         self.rescan()
635
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,
643                                      nodeName)
644         if not os.path.exists(self.rsyncDir):
645             return
646
647         rootBackupDirs = []
648
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))
655
656         for dirName in rootBackupDirs:
657             os.chdir(dirName)
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)
662                     fs = s2[0]
663
664                     rb = RsyncBackup ("%s/%s" %(dirName, d),
665                                       self.rsyncDir,
666                                       fs,
667                                       d,
668                                       os.stat(d).st_mtime)
669                     self.rsynced_backups.append (rb)
670
671     def __get_fs_mountpoints(self):
672         """Returns a dictionary mapping:
673            {filesystem : mountpoint}"""
674         result = {}
675         for filesys,mountpoint in self.datasets.list_filesystems():
676             result[filesys] = mountpoint
677         return result
678
679     def rescan(self):
680         cloned = self.datasets.list_cloned_snapshots()
681         self.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
686             try:
687                 cloned.index(snapname)
688             except ValueError:
689                 snapshot = zfs.Snapshot(snapname, snaptime)
690                 self.snapshots.append(snapshot)
691
692 class DeleteSnapshots(threading.Thread):
693
694     def __init__(self, snapshots):
695         threading.Thread.__init__(self)
696         self.backuptodelete = snapshots
697         self.started = False
698         self.completed = False
699         self.progress = 0.0
700         self.errors = []
701
702     def run(self):
703         deleted = 0
704         self.started = True
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.
711             if backup.exists():
712                 try:
713                     backup.destroy ()
714                 except RuntimeError, inst:
715                     self.errors.append(str(inst))
716             deleted += 1
717             self.progress = deleted / (total * 1.0)
718         self.completed = True
719
720 def main(argv):
721     try:
722         opts,args = getopt.getopt(sys.argv[1:], "", [])
723     except getopt.GetoptError:
724         sys.exit(2)
725     rbacp = RBACprofile()
726     if os.geteuid() == 0:
727         if len(args) > 0:
728             manager = DeleteSnapManager(args)
729         else:
730             manager = DeleteSnapManager()
731         gtk.gdk.threads_enter()
732         glib.idle_add(manager.initialise_view)
733         gtk.main()
734         gtk.gdk.threads_leave()
735     elif os.path.exists(argv) and os.path.exists("/usr/bin/gksu"):
736         # Run via gksu, which will prompt for the root password
737         newargs = ["gksu", argv]
738         for arg in args:
739             newargs.append(arg)
740         os.execv("/usr/bin/gksu", newargs);
741         # Shouldn't reach this point
742         sys.exit(1)
743     else:
744         dialog = gtk.MessageDialog(None,
745                                    0,
746                                    gtk.MESSAGE_ERROR,
747                                    gtk.BUTTONS_CLOSE,
748                                    _("Insufficient Priviliges"))
749         dialog.format_secondary_text(_("Snapshot deletion requires "
750                                        "administrative privileges to run. "
751                                        "You have not been assigned the necessary"
752                                        "administrative priviliges."
753                                        "\n\nConsult your system administrator "))
754         dialog.run()
755         print argv + "is not a valid executable path"
756         sys.exit(1)