Update README.md to specify the python requirements
[time-slider.git] / usr / share / time-slider / lib / time_slider / deletegui.py
1 #!/usr/bin/python3
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 from . 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 = str(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, 0o755)
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("couldn't delete %s, already used by another process" % self.mountpoint)
144             return
145
146         trashDir = os.path.join(self.rsync_dir,
147                           self.fsname,
148                           rsyncsmf.RSYNCTRASHSUFFIX)
149         if not os.path.exists(trashDir):
150             os.makedirs(trashDir, 0o755)
151
152         backupTrashDir = os.path.join (self.rsync_dir,
153                                  self.fsname,
154                                  rsyncsmf.RSYNCTRASHSUFFIX,
155                                  self.snaplabel)
156
157         # move then delete
158         os.rename (self.mountpoint, backupTrashDir)
159         shutil.rmtree (backupTrashDir)
160
161         log = "%s/%s/%s/%s.log" % (self.rsync_dir,
162                                    self.fsname,
163                                    rsyncsmf.RSYNCLOGSUFFIX,
164                                    self.snaplabel)
165         if os.path.exists (log):
166             os.unlink (log)
167
168         lockFp.close()
169         os.unlink(lockFile)
170
171 class DeleteSnapManager:
172
173     def __init__(self, snapshots = None):
174         self.xml = gtk.glade.XML("%s/../../glade/time-slider-delete.glade" \
175                                   % (os.path.dirname(__file__)))
176         self.backuptodelete = []
177         self.shortcircuit = []
178         maindialog = self.xml.get_widget("time-slider-delete")
179         self.pulsedialog = self.xml.get_widget("pulsedialog")
180         self.pulsedialog.set_transient_for(maindialog)
181         self.datasets = zfs.Datasets()
182         if snapshots:
183             maindialog.hide()
184             self.shortcircuit = snapshots
185         else:
186             glib.idle_add(self.__init_scan)
187
188         self.progressdialog = self.xml.get_widget("deletingdialog")
189         self.progressdialog.set_transient_for(maindialog)
190         self.progressbar = self.xml.get_widget("deletingprogress")
191         # signal dictionary
192         dic = {"on_closebutton_clicked" : gtk.main_quit,
193                "on_window_delete_event" : gtk.main_quit,
194                "on_snapshotmanager_delete_event" : gtk.main_quit,
195                "on_fsfilterentry_changed" : self.__on_filterentry_changed,
196                "on_schedfilterentry_changed" : self.__on_filterentry_changed,
197                "on_typefiltercombo_changed" : self.__on_filterentry_changed,
198                "on_selectbutton_clicked" : self.__on_selectbutton_clicked,
199                "on_deselectbutton_clicked" : self.__on_deselectbutton_clicked,
200                "on_deletebutton_clicked" : self.__on_deletebutton_clicked,
201                "on_confirmcancel_clicked" : self.__on_confirmcancel_clicked,
202                "on_confirmdelete_clicked" : self.__on_confirmdelete_clicked,
203                "on_errordialog_response" : self.__on_errordialog_response}
204         self.xml.signal_autoconnect(dic)
205
206     def initialise_view(self):
207         if len(self.shortcircuit) == 0:
208             # Set TreeViews
209             self.liststorefs = gtk.ListStore(str, str, str, str, str, int,
210                                              gobject.TYPE_PYOBJECT)
211             list_filter = self.liststorefs.filter_new()
212             list_sort = gtk.TreeModelSort(list_filter)
213             list_sort.set_sort_column_id(1, gtk.SORT_ASCENDING)
214
215             self.snaptreeview = self.xml.get_widget("snaplist")
216             self.snaptreeview.set_model(self.liststorefs)
217             self.snaptreeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
218
219             cell0 = gtk.CellRendererText()
220             cell1 = gtk.CellRendererText()
221             cell2 = gtk.CellRendererText()
222             cell3 = gtk.CellRendererText()
223             cell4 = gtk.CellRendererText()
224             cell5 = gtk.CellRendererText()
225
226             typecol = gtk.TreeViewColumn(_("Type"),
227                                             cell0, text = 0)
228             typecol.set_sort_column_id(0)
229             typecol.set_resizable(True)
230             typecol.connect("clicked",
231                 self.__on_treeviewcol_clicked, 0)
232             self.snaptreeview.append_column(typecol)
233
234             mountptcol = gtk.TreeViewColumn(_("Mount Point"),
235                                             cell1, text = 1)
236             mountptcol.set_sort_column_id(1)
237             mountptcol.set_resizable(True)
238             mountptcol.connect("clicked",
239                 self.__on_treeviewcol_clicked, 1)
240             self.snaptreeview.append_column(mountptcol)
241
242             fsnamecol = gtk.TreeViewColumn(_("File System Name"),
243                                            cell2, text = 2)
244             fsnamecol.set_sort_column_id(2)
245             fsnamecol.set_resizable(True)
246             fsnamecol.connect("clicked",
247                 self.__on_treeviewcol_clicked, 2)
248             self.snaptreeview.append_column(fsnamecol)
249
250             snaplabelcol = gtk.TreeViewColumn(_("Snapshot Name"),
251                                               cell3, text = 3)
252             snaplabelcol.set_sort_column_id(3)
253             snaplabelcol.set_resizable(True)
254             snaplabelcol.connect("clicked",
255                 self.__on_treeviewcol_clicked, 3)
256             self.snaptreeview.append_column(snaplabelcol)
257
258             cell4.props.xalign = 1.0
259             creationcol = gtk.TreeViewColumn(_("Creation Time"),
260                                              cell4, text = 4)
261             creationcol.set_sort_column_id(5)
262             creationcol.set_resizable(True)
263             creationcol.connect("clicked",
264                 self.__on_treeviewcol_clicked, 5)
265             self.snaptreeview.append_column(creationcol)
266
267             # Note to developers.
268             # The second element is for internal matching and should not
269             # be i18ned under any circumstances.
270             typestore = gtk.ListStore(str, str)
271             typestore.append([_("All"), "All"])
272             typestore.append([_("Backups"), "Backup"])
273             typestore.append([_("Snapshots"), "Snapshot"])
274
275             self.typefiltercombo = self.xml.get_widget("typefiltercombo")
276             self.typefiltercombo.set_model(typestore)
277             typefiltercomboCell = gtk.CellRendererText()
278             self.typefiltercombo.pack_start(typefiltercomboCell, True)
279             self.typefiltercombo.add_attribute(typefiltercomboCell, 'text',0)
280
281             # Note to developers.
282             # The second element is for internal matching and should not
283             # be i18ned under any circumstances.
284             fsstore = gtk.ListStore(str, str)
285             fslist = self.datasets.list_filesystems()
286             fsstore.append([_("All"), None])
287             for fsname,fsmount in fslist:
288                 fsstore.append([fsname, fsname])
289             self.fsfilterentry = self.xml.get_widget("fsfilterentry")
290             self.fsfilterentry.set_model(fsstore)
291             self.fsfilterentry.set_text_column(0)
292             fsfilterentryCell = gtk.CellRendererText()
293             self.fsfilterentry.pack_start(fsfilterentryCell)
294
295             schedstore = gtk.ListStore(str, str)
296             # Note to developers.
297             # The second element is for internal matching and should not
298             # be i18ned under any circumstances.
299             schedstore.append([_("All"), None])
300             schedstore.append([_("Monthly"), "monthly"])
301             schedstore.append([_("Weekly"), "weekly"])
302             schedstore.append([_("Daily"), "daily"])
303             schedstore.append([_("Hourly"), "hourly"])
304             schedstore.append([_("1/4 Hourly"), "frequent"])
305             self.schedfilterentry = self.xml.get_widget("schedfilterentry")
306             self.schedfilterentry.set_model(schedstore)
307             self.schedfilterentry.set_text_column(0)
308             schedentryCell = gtk.CellRendererText()
309             self.schedfilterentry.pack_start(schedentryCell)
310
311             self.schedfilterentry.set_active(0)
312             self.fsfilterentry.set_active(0)
313             self.typefiltercombo.set_active(0)
314         else:
315             cloned = self.datasets.list_cloned_snapshots()
316             num_snap = 0
317             num_rsync = 0
318             for snapname in self.shortcircuit:
319                 # Filter out snapshots that are the root
320                 # of cloned filesystems or volumes
321                 try:
322                     cloned.index(snapname)
323                     dialog = gtk.MessageDialog(None,
324                                    0,
325                                    gtk.MESSAGE_ERROR,
326                                    gtk.BUTTONS_CLOSE,
327                                    _("Snapshot can not be deleted"))
328                     text = _("%s has one or more dependent clones "
329                              "and will not be deleted. To delete "
330                              "this snapshot, first delete all "
331                              "datasets and snapshots cloned from "
332                              "this snapshot.") \
333                              % snapname
334                     dialog.format_secondary_text(text)
335                     dialog.run()
336                     sys.exit(1)
337                 except ValueError:
338                     path = os.path.abspath (snapname)
339                     if not os.path.exists (path):
340                         snapshot = zfs.Snapshot(snapname)
341                         self.backuptodelete.append(snapshot)
342                         num_snap += 1
343                     else:
344                         self.backuptodelete.append(RsyncBackup (snapname))
345                         num_rsync += 1
346
347             confirm = self.xml.get_widget("confirmdialog")
348             summary = self.xml.get_widget("summarylabel")
349             total = len(self.backuptodelete)
350
351             text = ""
352             if num_rsync != 0 :
353                 if num_rsync == 1:
354                     text = _("1 external backup will be deleted.")
355                 else:
356                     text = _("%d external backups will be deleted.") % num_rsync
357
358             if num_snap != 0 :
359                 if len(text) != 0:
360                     text += "\n"
361                 if num_snap == 1:
362                     text += _("1 snapshot will be deleted.")
363                 else:
364                     text += _("%d snapshots will be deleted.") % num_snap
365
366             summary.set_text(text )
367             response = confirm.run()
368             if response != 2:
369                 sys.exit(0)
370             else:
371                 # Create the thread in an idle loop in order to
372                 # avoid deadlock inside gtk.
373                 glib.idle_add(self.__init_delete)
374         return False
375
376     def __on_treeviewcol_clicked(self, widget, searchcol):
377         self.snaptreeview.set_search_column(searchcol)
378
379     def __filter_snapshot_list(self, list, filesys = None, snap = None, btype = None):
380         if filesys == None and snap == None and btype == None:
381             return list
382         fssublist = []
383         if filesys != None:
384             for snapshot in list:
385                 if snapshot.fsname.find(filesys) != -1:
386                     fssublist.append(snapshot)
387         else:
388             fssublist = list
389
390         snaplist = []
391         if snap != None:
392             for snapshot in fssublist:
393                 if  snapshot.snaplabel.find(snap) != -1:
394                     snaplist.append(snapshot)
395         else:
396             snaplist = fssublist
397
398         typelist = []
399         if btype != None and btype != "All":
400             for item in snaplist:
401                 if btype == "Backup":
402                     if isinstance(item, RsyncBackup):
403                         typelist.append (item)
404                 else:
405                     if isinstance(item, zfs.Snapshot):
406                         typelist.append (item)
407         else:
408             typelist = snaplist
409
410         return typelist
411
412     def __on_filterentry_changed(self, widget):
413         # Get the filesystem filter value
414         iter = self.fsfilterentry.get_active_iter()
415         if iter == None:
416             filesys = self.fsfilterentry.get_active_text()
417         else:
418             model = self.fsfilterentry.get_model()
419             filesys = model.get(iter, 1)[0]
420         # Get the snapshot name filter value
421         iter = self.schedfilterentry.get_active_iter()
422         if iter == None:
423             snap = self.schedfilterentry.get_active_text()
424         else:
425             model = self.schedfilterentry.get_model()
426             snap = model.get(iter, 1)[0]
427
428         # Get the type filter value
429         iter = self.typefiltercombo.get_active_iter()
430         if iter == None:
431             type = "All"
432         else:
433             model = self.typefiltercombo.get_model()
434             type = model.get(iter, 1)[0]
435
436         self.liststorefs.clear()
437         newlist = self.__filter_snapshot_list(self.snapscanner.snapshots,
438                     filesys,
439                     snap, type)
440         for snapshot in newlist:
441             try:
442                 tm = time.localtime(snapshot.get_creation_time())
443                 t = str(time.strftime ("%c", tm),
444                     locale.getpreferredencoding()).encode('utf-8')
445             except:
446                 t = time.ctime(snapshot.get_creation_time())
447             try:
448                 mount_point = self.snapscanner.mounts[snapshot.fsname]
449                 if (mount_point == "legacy"):
450                     mount_point = _("Legacy")
451
452                 self.liststorefs.append([
453                        _("Snapshot"),
454                        mount_point,
455                        snapshot.fsname,
456                        snapshot.snaplabel,
457                        t,
458                        snapshot.get_creation_time(),
459                        snapshot])
460             except KeyError:
461                 continue
462                 # This will catch exceptions from things we ignore
463                 # such as dump as swap volumes and skip over them.
464             # add rsync backups
465         newlist = self.__filter_snapshot_list(self.snapscanner.rsynced_backups,
466                                                 filesys,
467                                                 snap, type)
468         for backup in newlist:
469             self.liststorefs.append([_("Backup"),
470                                      backup.zfs_mountpoint,
471                                      backup.fsname,
472                                      backup.snaplabel,
473                                      backup.creationtime_str,
474                                      backup.creationtime,
475                                      backup])
476
477     def __on_selectbutton_clicked(self, widget):
478         selection = self.snaptreeview.get_selection()
479         selection.select_all()
480         return
481
482     def __on_deselectbutton_clicked(self, widget):
483         selection = self.snaptreeview.get_selection()
484         selection.unselect_all()
485         return
486
487     def __on_deletebutton_clicked(self, widget):
488         self.backuptodelete = []
489         selection = self.snaptreeview.get_selection()
490         selection.selected_foreach(self.__add_selection)
491         total = len(self.backuptodelete)
492         if total <= 0:
493             return
494
495         confirm = self.xml.get_widget("confirmdialog")
496         summary = self.xml.get_widget("summarylabel")
497
498         num_snap = 0
499         num_rsync = 0
500         for item in self.backuptodelete:
501             if isinstance (item, RsyncBackup):
502                 num_rsync+=1
503             else:
504                 num_snap+=1
505
506         str = ""
507         if num_rsync != 0 :
508             if num_rsync == 1:
509                 str = _("1 external backup will be deleted.")
510             else:
511                 str = _("%d external backups will be deleted.") % num_rsync
512
513         if num_snap != 0 :
514             if len(str) != 0:
515                 str += "\n"
516             if num_snap == 1:
517                 str += _("1 snapshot will be deleted.")
518             else:
519                 str += _("%d snapshots will be deleted.") % num_snap
520
521         summary.set_text(str)
522         response = confirm.run()
523         if response != 2:
524             return
525         else:
526             glib.idle_add(self.__init_delete)
527         return
528
529     def __init_scan(self):
530         self.snapscanner = ScanSnapshots()
531         self.pulsedialog.show()
532         self.snapscanner.start()
533         glib.timeout_add(100, self.__monitor_scan)
534         return False
535
536     def __init_delete(self):
537         self.snapdeleter = DeleteSnapshots(self.backuptodelete)
538         # If there's more than a few snapshots, pop up
539         # a progress bar.
540         if len(self.backuptodelete) > 3:
541             self.progressbar.set_fraction(0.0)
542             self.progressdialog.show()
543         self.snapdeleter.start()
544         glib.timeout_add(300, self.__monitor_deletion)
545         return False
546
547     def __monitor_scan(self):
548         if self.snapscanner.isAlive() == True:
549             self.xml.get_widget("pulsebar").pulse()
550             return True
551         else:
552             self.pulsedialog.hide()
553             if self.snapscanner.errors:
554                 details = ""
555                 dialog = gtk.MessageDialog(None,
556                             0,
557                             gtk.MESSAGE_ERROR,
558                             gtk.BUTTONS_CLOSE,
559                             _("Some snapshots could not be read"))
560                 dialog.connect("response",
561                             self.__on_errordialog_response)
562                 for error in self.snapscanner.errors:
563                     details = details + error
564                 dialog.format_secondary_text(details)
565                 dialog.show()
566             self.__on_filterentry_changed(None)
567             return False
568
569     def __monitor_deletion(self):
570         if self.snapdeleter.isAlive() == True:
571             self.progressbar.set_fraction(self.snapdeleter.progress)
572             return True
573         else:
574             self.progressdialog.hide()
575             self.progressbar.set_fraction(1.0)
576             self.progressdialog.hide()
577             if self.snapdeleter.errors:
578                 details = ""
579                 dialog = gtk.MessageDialog(None,
580                             0,
581                             gtk.MESSAGE_ERROR,
582                             gtk.BUTTONS_CLOSE,
583                             _("Some snapshots could not be deleted"))
584                 dialog.connect("response",
585                             self.__on_errordialog_response)
586                 for error in self.snapdeleter.errors:
587                     details = details + error
588                 dialog.format_secondary_text(details)
589                 dialog.show()
590             # If we didn't shortcircut straight to the delete confirmation
591             # dialog then the main dialog is visible so we rebuild the list
592             # view.
593             if len(self.shortcircuit) ==  0:
594                 self.__refresh_view()
595             else:
596                 gtk.main_quit()
597             return False
598
599     def __refresh_view(self):
600         self.liststorefs.clear()
601         glib.idle_add(self.__init_scan)
602         self.backuptodelete = []
603
604     def __add_selection(self, treemodel, path, iter):
605         snapshot = treemodel.get(iter, 6)[0]
606         self.backuptodelete.append(snapshot)
607
608     def __on_confirmcancel_clicked(self, widget):
609         widget.get_toplevel().hide()
610         widget.get_toplevel().response(1)
611
612     def __on_confirmdelete_clicked(self, widget):
613         widget.get_toplevel().hide()
614         widget.get_toplevel().response(2)
615
616     def __on_errordialog_response(self, widget, responseid):
617         widget.hide()
618
619 class ScanSnapshots(threading.Thread):
620
621     def __init__(self):
622         threading.Thread.__init__(self)
623         self.errors = []
624         self.datasets = zfs.Datasets()
625         self.snapshots = []
626         self.rsynced_fs = []
627         self.rsynced_backups = []
628
629     def run(self):
630         self.mounts = self.__get_fs_mountpoints()
631         self.rsyncsmf = rsyncsmf.RsyncSMF("%s:rsync" %(plugin.PLUGINBASEFMRI))
632         self.__get_rsync_backups ()
633         self.rescan()
634
635     def __get_rsync_backups (self):
636         # get rsync backup dir
637         self.rsyncsmf = rsyncsmf.RsyncSMF("%s:rsync" %(plugin.PLUGINBASEFMRI))
638         rsyncBaseDir = self.rsyncsmf.get_target_dir()
639         sys,nodeName,rel,ver,arch = os.uname()
640         self.rsyncDir = os.path.join(rsyncBaseDir,
641                                      rsyncsmf.RSYNCDIRPREFIX,
642                                      nodeName)
643         if not os.path.exists(self.rsyncDir):
644             return
645
646         rootBackupDirs = []
647
648         for root, dirs, files in os.walk(self.rsyncDir):
649             if '.time-slider' in dirs:
650                 dirs.remove('.time-slider')
651                 backupDir = os.path.join(root, rsyncsmf.RSYNCDIRSUFFIX)
652                 if os.path.exists(backupDir):
653                     insort(rootBackupDirs, os.path.abspath(backupDir))
654
655         for dirName in rootBackupDirs:
656             os.chdir(dirName)
657             for d in os.listdir(dirName):
658                 if os.path.isdir(d) and not os.path.islink(d):
659                     s1 = dirName.split ("%s/" % self.rsyncDir, 1)
660                     s2 = s1[1].split ("/%s" % rsyncsmf.RSYNCDIRSUFFIX, 1)
661                     fs = s2[0]
662
663                     rb = RsyncBackup ("%s/%s" %(dirName, d),
664                                       self.rsyncDir,
665                                       fs,
666                                       d,
667                                       os.stat(d).st_mtime)
668                     self.rsynced_backups.append (rb)
669
670     def __get_fs_mountpoints(self):
671         """Returns a dictionary mapping:
672            {filesystem : mountpoint}"""
673         result = {}
674         for filesys,mountpoint in self.datasets.list_filesystems():
675             result[filesys] = mountpoint
676         return result
677
678     def rescan(self):
679         cloned = self.datasets.list_cloned_snapshots()
680         self.snapshots = []
681         snaplist = self.datasets.list_snapshots()
682         for snapname,snaptime in snaplist:
683             # Filter out snapshots that are the root
684             # of cloned filesystems or volumes
685             try:
686                 cloned.index(snapname)
687             except ValueError:
688                 snapshot = zfs.Snapshot(snapname, snaptime)
689                 self.snapshots.append(snapshot)
690
691 class DeleteSnapshots(threading.Thread):
692
693     def __init__(self, snapshots):
694         threading.Thread.__init__(self)
695         self.backuptodelete = snapshots
696         self.started = False
697         self.completed = False
698         self.progress = 0.0
699         self.errors = []
700
701     def run(self):
702         deleted = 0
703         self.started = True
704         total = len(self.backuptodelete)
705         for backup in self.backuptodelete:
706             # The backup could have expired and been automatically
707             # destroyed since the user selected it. Check that it
708             # still exists before attempting to delete it. If it
709             # doesn't exist just silently ignore it.
710             if backup.exists():
711                 try:
712                     backup.destroy ()
713                 except RuntimeError as inst:
714                     self.errors.append(str(inst))
715             deleted += 1
716             self.progress = deleted / (total * 1.0)
717         self.completed = True
718
719 def main(argv):
720     try:
721         opts,args = getopt.getopt(sys.argv[1:], "", [])
722     except getopt.GetoptError:
723         sys.exit(2)
724     rbacp = RBACprofile()
725     if os.geteuid() == 0:
726         if len(args) > 0:
727             manager = DeleteSnapManager(args)
728         else:
729             manager = DeleteSnapManager()
730         gtk.gdk.threads_enter()
731         glib.idle_add(manager.initialise_view)
732         gtk.main()
733         gtk.gdk.threads_leave()
734     elif os.path.exists(argv) and os.path.exists("/usr/bin/gksu"):
735         # Run via gksu, which will prompt for the root password
736         newargs = ["gksu", argv]
737         for arg in args:
738             newargs.append(arg)
739         os.execv("/usr/bin/gksu", newargs);
740         # Shouldn't reach this point
741         sys.exit(1)
742     else:
743         dialog = gtk.MessageDialog(None,
744                                    0,
745                                    gtk.MESSAGE_ERROR,
746                                    gtk.BUTTONS_CLOSE,
747                                    _("Insufficient Priviliges"))
748         dialog.format_secondary_text(_("Snapshot deletion requires "
749                                        "administrative privileges to run. "
750                                        "You have not been assigned the necessary"
751                                        "administrative priviliges."
752                                        "\n\nConsult your system administrator "))
753         dialog.run()
754         print(argv + "is not a valid executable path")
755         sys.exit(1)