Replace all mentions of /usr/bin/python2.6 with /usr/bin/python2
[time-slider.git] / usr / share / time-slider / lib / time_slider / setupgui.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 sys
24 import os
25 import subprocess
26 import threading
27 import util
28 import smf
29 from autosnapsmf import enable_default_schedules, disable_default_schedules
30
31 from os.path import abspath, dirname, join, pardir
32 sys.path.insert(0, join(dirname(__file__), pardir, "plugin"))
33 import plugin
34 sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync"))
35 import rsyncsmf
36
37 try:
38     import pygtk
39     pygtk.require("2.4")
40 except:
41     pass
42 try:
43     import gtk
44     import gtk.glade
45     gtk.gdk.threads_init()
46 except:
47     sys.exit(1)
48
49 import glib
50 import gobject
51 import gio
52 import dbus
53 import dbus.service
54 import dbus.mainloop
55 import dbus.mainloop.glib
56 import dbussvc
57
58
59 # This is the rough guess ratio used for rsync backup device size
60 # vs. the total size of the pools it's expected to backup.
61 RSYNCTARGETRATIO = 2
62
63 # here we define the path constants so that other modules can use it.
64 # this allows us to get access to the shared files without having to
65 # know the actual location, we just use the location of the current
66 # file and use paths relative to that.
67 SHARED_FILES = os.path.abspath(os.path.join(os.path.dirname(__file__),
68                                os.path.pardir,
69                                os.path.pardir))
70 LOCALE_PATH = os.path.join('/usr', 'share', 'locale')
71 RESOURCE_PATH = os.path.join(SHARED_FILES, 'res')
72
73 # the name of the gettext domain. because we have our translation files
74 # not in a global folder this doesn't really matter, setting it to the
75 # application name is a good idea tough.
76 GETTEXT_DOMAIN = 'time-slider'
77
78 # set up the glade gettext system and locales
79 gtk.glade.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH)
80 gtk.glade.textdomain(GETTEXT_DOMAIN)
81
82 import zfs
83 from timeslidersmf import TimeSliderSMF
84 from rbac import RBACprofile
85
86
87 class FilesystemIntention:
88
89     def __init__(self, name, selected, inherited):
90         self.name = name
91         self.selected = selected
92         self.inherited = inherited
93
94     def __str__(self):
95         return_string = "Filesystem name: " + self.name + \
96                 "\n\tSelected: " + str(self.selected) + \
97                 "\n\tInherited: " + str(self.inherited)
98         return return_string
99
100     def __eq__(self, other):
101         if self.name != other.name:
102             return False
103         if self.inherited and other.inherited:
104             return True
105         elif not self.inherited and other.inherited:
106             return False
107         if (self.selected == other.selected) and \
108            (self.inherited == other.inherited):
109             return True
110         else:
111             return False
112
113 class SetupManager:
114
115     def __init__(self, execpath):
116         self._execPath = execpath
117         self._datasets = zfs.Datasets()
118         self._xml = gtk.glade.XML("%s/../../glade/time-slider-setup.glade" \
119                                   % (os.path.dirname(__file__)))
120
121         # Tell dbus to use the gobject mainloop for async ops
122         dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
123         dbus.mainloop.glib.threads_init()
124
125         # Register a bus name with the system dbus daemon
126         systemBus = dbus.SystemBus()
127         busName = dbus.service.BusName("org.opensolaris.TimeSlider.config",
128                                        systemBus)
129         self._dbus = dbussvc.Config(systemBus,
130                                     '/org/opensolaris/TimeSlider/config')
131         # Used later to trigger a D-Bus notification of select configuration 
132         # changes made
133         self._configNotify = False
134
135         # These variables record the initial UI state which are used
136         # later to compare against the UI state when the OK or Cancel
137         # button is clicked and apply the minimum set of necessary 
138         # configuration changes. Prevents minor changes taking ages
139         # to be applied by the GUI.
140         self._initialEnabledState = None
141         self._initialRsyncState = None
142         self._initialRsyncTargetDir = None
143         self._initialCleanupLevel = None
144         self._initialCustomSelection = False
145         self._initialSnapStateDic = {}
146         self._initialRsyncStateDic = {}
147         self._initialFsIntentDic = {}
148         self._initialRsyncIntentDic = {}
149
150         # Currently selected rsync backup device via the GUI.
151         self._newRsyncTargetDir = None
152         # Used to store GUI filesystem selection state and the
153         # set of intended properties to apply to zfs filesystems.
154         self._snapStateDic = {}
155         self._rsyncStateDic = {}
156         self._fsIntentDic = {}
157         self._rsyncIntentDic = {}
158         # Dictionary that maps device ID numbers to zfs filesystem objects
159         self._fsDevices = {}
160
161         topLevel = self._xml.get_widget("toplevel")
162         self._pulseDialog = self._xml.get_widget("pulsedialog")
163         self._pulseDialog.set_transient_for(topLevel)
164         
165         # gio.VolumeMonitor reference
166         self._vm = gio.volume_monitor_get()
167         self._vm.connect("mount-added", self._mount_added)
168         self._vm.connect("mount-removed" , self._mount_removed)
169
170         self._fsListStore = gtk.ListStore(bool,
171                                          bool,
172                                          str,
173                                          str,
174                                          gobject.TYPE_PYOBJECT)
175         filesystems = self._datasets.list_filesystems()
176         for fsname,fsmountpoint in filesystems:
177             if (fsmountpoint == "legacy"):
178                 mountpoint = _("Legacy")
179             else:
180                 mountpoint = fsmountpoint
181             fs = zfs.Filesystem(fsname, fsmountpoint)
182             # Note that we don't deal with legacy mountpoints.
183             if fsmountpoint != "legacy" and fs.is_mounted():
184                 self._fsDevices[os.stat(fsmountpoint).st_dev] = fs
185             snap = fs.get_auto_snap()
186             rsyncstr = fs.get_user_property(rsyncsmf.RSYNCFSTAG)
187             if rsyncstr == "true":
188                 rsync = True
189             else:
190                 rsync = False
191             # Rsync is only performed on snapshotted filesystems.
192             # So treat as False if rsync is set to true independently
193             self._fsListStore.append([snap, snap & rsync,
194                                      mountpoint, fs.name, fs])
195             self._initialSnapStateDic[fs.name] = snap
196             self._initialRsyncStateDic[fs.name] = snap & rsync
197         del filesystems
198
199         for fsname in self._initialSnapStateDic:
200                 self._refine_filesys_actions(fsname,
201                                               self._initialSnapStateDic,
202                                               self._initialFsIntentDic)
203                 self._refine_filesys_actions(fsname,
204                                               self._initialRsyncStateDic,
205                                               self._initialRsyncIntentDic)
206    
207         self._fsTreeView = self._xml.get_widget("fstreeview")
208         self._fsTreeView.set_sensitive(False)
209         self._fsTreeView.set_size_request(10, 200)
210
211         self._fsTreeView.set_model(self._fsListStore)
212
213         cell0 = gtk.CellRendererToggle()
214         cell1 = gtk.CellRendererToggle()
215         cell2 = gtk.CellRendererText()
216         cell3 = gtk.CellRendererText()
217  
218         radioColumn = gtk.TreeViewColumn(_("Select"),
219                                              cell0, active=0)
220         self._fsTreeView.insert_column(radioColumn, 0)
221
222         self._rsyncRadioColumn = gtk.TreeViewColumn(_("Replicate"),
223                                                     cell1, active=1)
224         nameColumn = gtk.TreeViewColumn(_("Mount Point"),
225                                         cell2, text=2)
226         self._fsTreeView.insert_column(nameColumn, 2)
227         mountPointColumn = gtk.TreeViewColumn(_("File System Name"),
228                                               cell3, text=3)
229         self._fsTreeView.insert_column(mountPointColumn, 3)
230         cell0.connect('toggled', self._row_toggled)
231         cell1.connect('toggled', self._rsync_cell_toggled)
232         advancedBox = self._xml.get_widget("advancedbox")
233         advancedBox.connect('unmap', self._advancedbox_unmap)  
234
235         self._rsyncSMF = rsyncsmf.RsyncSMF("%s:rsync" \
236                                           %(plugin.PLUGINBASEFMRI))
237         state = self._rsyncSMF.get_service_state()
238         self._initialRsyncTargetDir = self._rsyncSMF.get_target_dir()
239         # Check for the default, unset value of "" from SMF.
240         if self._initialRsyncTargetDir == '""':
241             self._initialRsyncTargetDir = ''
242         self._newRsyncTargetDir = self._initialRsyncTargetDir
243         self._smfTargetKey = self._rsyncSMF.get_target_key()
244         self._newRsyncTargetSelected = False
245         sys,self._nodeName,rel,ver,arch = os.uname()
246
247         # Model columns:
248         # 0 Themed icon list (python list)
249         # 1 device root
250         # 2 volume name
251         # 3 Is gio.Mount device
252         # 4 Is separator (for comboBox separator rendering)
253         self._rsyncStore = gtk.ListStore(gobject.TYPE_PYOBJECT,
254                                          gobject.TYPE_STRING,
255                                          gobject.TYPE_STRING,
256                                          gobject.TYPE_BOOLEAN,
257                                          gobject.TYPE_BOOLEAN)
258         self._rsyncCombo = self._xml.get_widget("rsyncdevcombo")
259         mounts = self._vm.get_mounts()
260         for mount in mounts:
261             self._mount_added(self._vm, mount)
262         if len(mounts) > 0:
263             # Add a separator
264             self._rsyncStore.append((None, None, None, None, True))
265         del mounts
266
267         if len(self._newRsyncTargetDir) == 0:
268             self._rsyncStore.append((['folder'],
269                                     _("(None)"),
270                                     '',
271                                     False,
272                                     False))
273             # Add a separator
274             self._rsyncStore.append((None, None, None, None, True))
275         self._rsyncStore.append((None, _("Other..."), "Other", False, False))
276         self._iconCell = gtk.CellRendererPixbuf()
277         self._nameCell = gtk.CellRendererText()
278         self._rsyncCombo.clear()
279         self._rsyncCombo.pack_start(self._iconCell, False)
280         self._rsyncCombo.set_cell_data_func(self._iconCell,
281                                             self._icon_cell_render)
282         self._rsyncCombo.pack_end(self._nameCell)
283         self._rsyncCombo.set_attributes(self._nameCell, text=1)
284         self._rsyncCombo.set_row_separator_func(self._row_separator)
285         self._rsyncCombo.set_model(self._rsyncStore)
286         self._rsyncCombo.connect("changed", self._rsync_combo_changed)
287         # Force selection of currently configured device
288         self._rsync_dev_selected(self._newRsyncTargetDir)
289
290         # signal dictionary     
291         dic = {"on_ok_clicked" : self._on_ok_clicked,
292                "on_cancel_clicked" : gtk.main_quit,
293                "on_snapshotmanager_delete_event" : gtk.main_quit,
294                "on_enablebutton_toggled" : self._on_enablebutton_toggled,
295                "on_rsyncbutton_toggled" : self._on_rsyncbutton_toggled,
296                "on_defaultfsradio_toggled" : self._on_defaultfsradio_toggled,
297                "on_selectfsradio_toggled" : self._on_selectfsradio_toggled,
298                "on_deletesnapshots_clicked" : self._on_deletesnapshots_clicked}
299         self._xml.signal_autoconnect(dic)
300
301         if state != "disabled":
302             self._rsyncEnabled = True
303             self._xml.get_widget("rsyncbutton").set_active(True)
304             self._initialRsyncState = True
305         else:
306             self._rsyncEnabled = False
307             self._rsyncCombo.set_sensitive(False)
308             self._initialRsyncState = False
309
310         # Initialise SMF service instance state.
311         try:
312             self._sliderSMF = TimeSliderSMF()
313         except RuntimeError,message:
314             self._xml.get_widget("toplevel").set_sensitive(False)
315             dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"),
316                                        0,
317                                        gtk.MESSAGE_ERROR,
318                                        gtk.BUTTONS_CLOSE,
319                                        _("Snapshot manager service error"))
320             dialog.format_secondary_text(_("The snapshot manager service does "
321                                          "not appear to be installed on this "
322                                          "system."
323                                          "\n\nSee the svcs(1) man page for more "
324                                          "information."
325                                          "\n\nDetails:\n%s")%(message))
326             dialog.set_icon_name("time-slider-setup")
327             dialog.run()
328             sys.exit(1)
329
330         if self._sliderSMF.svcstate == "disabled":
331             self._xml.get_widget("enablebutton").set_active(False)
332             self._initialEnabledState = False
333         elif self._sliderSMF.svcstate == "offline":
334             self._xml.get_widget("toplevel").set_sensitive(False)
335             errors = ''.join("%s\n" % (error) for error in \
336                 self._sliderSMF.find_dependency_errors())
337             dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"),
338                                         0,
339                                         gtk.MESSAGE_ERROR,
340                                         gtk.BUTTONS_CLOSE,
341                                         _("Snapshot manager service dependency error"))
342             dialog.format_secondary_text(_("The snapshot manager service has "
343                                             "been placed offline due to a dependency "
344                                             "problem. The following dependency problems "
345                                             "were found:\n\n%s\n\nRun \"svcs -xv\" from "
346                                             "a command prompt for more information about "
347                                             "these dependency problems.") % errors)
348             dialog.set_icon_name("time-slider-setup")
349             dialog.run()
350             sys.exit(1)
351         elif self._sliderSMF.svcstate == "maintenance":
352             self._xml.get_widget("toplevel").set_sensitive(False)
353             dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"),
354                                         0,
355                                         gtk.MESSAGE_ERROR,
356                                         gtk.BUTTONS_CLOSE,
357                                         _("Snapshot manager service error"))
358             dialog.format_secondary_text(_("The snapshot manager service has "
359                                             "encountered a problem and has been "
360                                             "disabled until the problem is fixed."
361                                             "\n\nSee the svcs(1) man page for more "
362                                             "information."))
363             dialog.set_icon_name("time-slider-setup")
364             dialog.run()
365             sys.exit(1)
366         else:
367             # FIXME: Check transitional states 
368             self._xml.get_widget("enablebutton").set_active(True)
369             self._initialEnabledState = True
370
371
372         # Emit a toggled signal so that the initial GUI state is consistent
373         self._xml.get_widget("enablebutton").emit("toggled")
374         # Check the snapshotting policy (UserData (default), or Custom)
375         self._initialCustomSelection = self._sliderSMF.is_custom_selection()
376         if self._initialCustomSelection == True:
377             self._xml.get_widget("selectfsradio").set_active(True)
378             # Show the advanced controls so the user can see the
379             # customised configuration.
380             if self._sliderSMF.svcstate != "disabled":
381                 self._xml.get_widget("expander").set_expanded(True)
382         else: # "false" or any other non "true" value
383             self._xml.get_widget("defaultfsradio").set_active(True)
384
385         # Set the cleanup threshhold value
386         spinButton = self._xml.get_widget("capspinbutton")
387         critLevel = self._sliderSMF.get_cleanup_level("critical")
388         warnLevel = self._sliderSMF.get_cleanup_level("warning")
389
390         # Force the warning level to something practical
391         # on the lower end, and make it no greater than the
392         # critical level specified in the SVC instance.
393         spinButton.set_range(70, critLevel)
394         self._initialCleanupLevel = warnLevel
395         if warnLevel > 70:
396             spinButton.set_value(warnLevel)
397         else:
398             spinButton.set_value(70)
399
400     def _icon_cell_render(self, celllayout, cell, model, iter):
401         iconList = self._rsyncStore.get_value(iter, 0)
402         if iconList != None:
403             gicon = gio.ThemedIcon(iconList)
404             cell.set_property("gicon", gicon)
405         else:
406             root = self._rsyncStore.get_value(iter, 2)
407             if root == "Other":
408                 cell.set_property("gicon", None)
409
410     def _row_separator(self, model, iter):
411         return model.get_value(iter, 4)
412
413     def _mount_added(self, volume_monitor, mount):
414         icon = mount.get_icon()
415         iconList = icon.get_names()
416         if iconList == None:
417             iconList = ['drive-harddisk', 'drive']
418         root = mount.get_root()
419         path = root.get_path()
420         mountName = mount.get_name()
421         volume = mount.get_volume()
422         if volume == None:
423             volName = mount.get_name()
424             if volName == None:
425                 volName = os.path.split(path)[1]
426         else:
427             volName = volume.get_name()
428
429         # Check to see if there is at least one gio.Mount device already
430         # in the ListStore. If not, then we also need to add a separator
431         # row.
432         iter = self._rsyncStore.get_iter_first()
433         if iter and self._rsyncStore.get_value(iter, 3) == False:
434             self._rsyncStore.insert(0, (None, None, None, None, True))
435         
436         self._rsyncStore.insert(0, (iconList, volName, path, True, False))
437         # If this happens to be the already configured backup device
438         # and the user hasn't tried to change device yet, auto select
439         # it.
440         if self._initialRsyncTargetDir == self._newRsyncTargetDir:
441             if self._validate_rsync_target(path) == True:
442                 self._rsyncCombo.set_active(0)
443
444     def _mount_removed(self, volume_monitor, mount):
445         root = mount.get_root()
446         path = root.get_path()
447         iter = self._rsyncStore.get_iter_first()
448         mountIter = None
449         numMounts = 0
450         # Search gio.Mount devices
451         while iter != None and \
452             self._rsyncStore.get_value(iter, 3) == True:
453             numMounts += 1
454             compPath = self._rsyncStore.get_value(iter, 2)
455             if compPath == path:
456                 mountIter = iter
457                 break
458             else:
459                 iter = self._rsyncStore.iter_next(iter)
460         if mountIter != None:
461             if numMounts == 1:
462                 # Need to remove the separator also since
463                 # there will be no more gio.Mount devices
464                 # shown in the combo box
465                 sepIter = self._rsyncStore.iter_next(mountIter)
466                 if self._rsyncStore.get_value(sepIter, 4) == True:
467                     self._rsyncStore.remove(sepIter)                  
468             self._rsyncStore.remove(mountIter)
469             iter = self._rsyncStore.get_iter_first()
470             # Insert a custom folder if none exists already
471             if self._rsyncStore.get_value(iter, 2) == "Other":
472                 path = self._initialRsyncTargetDir
473                 length = len(path)
474                 if length > 1:
475                     name = os.path.split(path)[1]
476                 elif length == 1:
477                     name = path
478                 else: # Indicates path is unset: ''
479                     name = _("(None)")
480                 iter = self._rsyncStore.insert_before(iter,
481                                                       (None,
482                                                        None,
483                                                        None,
484                                                        None,
485                                                        True))
486                 iter = self._rsyncStore.insert_before(iter,
487                                                       (['folder'],
488                                                        name,
489                                                        path,
490                                                        False,
491                                                        False))
492             self._rsyncCombo.set_active_iter(iter)
493
494     def _monitor_setup(self, pulseBar):
495         if self._enabler.isAlive() == True:
496             pulseBar.pulse()
497             return True
498         else:
499             gtk.main_quit()   
500
501     def _row_toggled(self, renderer, path):
502         model = self._fsTreeView.get_model()
503         iter = model.get_iter(path)
504         state = renderer.get_active()
505         if state == False:
506             self._fsListStore.set_value(iter, 0, True)
507         else:
508             self._fsListStore.set_value(iter, 0, False)
509             self._fsListStore.set_value(iter, 1, False)
510
511     def _rsync_cell_toggled(self, renderer, path):
512         model = self._fsTreeView.get_model()
513         iter = model.get_iter(path)
514         state = renderer.get_active()
515         rowstate = self._fsListStore.get_value(iter, 0)
516         if rowstate == True:
517             if state == False:
518                 self._fsListStore.set_value(iter, 1, True)
519             else:
520                 self._fsListStore.set_value(iter, 1, False)
521
522     def _rsync_config_error(self, msg):
523         topLevel = self._xml.get_widget("toplevel")
524         dialog = gtk.MessageDialog(topLevel,
525                                     0,
526                                     gtk.MESSAGE_ERROR,
527                                     gtk.BUTTONS_CLOSE,
528                                     _("Unsuitable Backup Location"))
529         dialog.format_secondary_text(msg)
530         dialog.set_icon_name("time-slider-setup")
531         dialog.run()
532         dialog.hide()
533         return
534
535     def _rsync_dev_selected(self, path):
536         iter = self._rsyncStore.get_iter_first()
537         while iter != None:
538             # Break out when we hit a non gio.Mount device
539             if self._rsyncStore.get_value(iter, 3) == False:
540                 break
541             compPath = self._rsyncStore.get_value(iter, 2)
542             if compPath == path:
543                 self._rsyncCombo.set_active_iter(iter)
544                 self._newRsyncTargetDir = path
545                 return
546             else:
547                 iter = self._rsyncStore.iter_next(iter)
548
549         # Not one of the shortcut RMM devices, so it's
550         # some other path on the filesystem.
551         # iter may be pointing at a separator. Increment
552         # to next row iter if so.
553         if self._rsyncStore.get_value(iter, 4) == True:
554             iter = self._rsyncStore.iter_next(iter)
555
556         if iter != None:
557             if len(path) > 1:
558                 name = os.path.split(path)[1]
559             elif len(path) == 1:
560                 name = path
561             else: # Indicates path is unset: ''
562                 name = _("(None)")
563             # Could be either the custom folder selection
564             # row or the  "Other" row if the custom row
565             # was not created. If "Other" then create the
566             # custom row and separator now at this position
567             if self._rsyncStore.get_value(iter, 2) == "Other":
568                 iter = self._rsyncStore.insert_before(iter,
569                                                       (None,
570                                                        None,
571                                                        None,
572                                                        None,
573                                                        True))
574                 iter = self._rsyncStore.insert_before(iter,
575                                                       (['folder'],
576                                                        name,
577                                                        path,
578                                                        False,
579                                                        False))
580             else:
581                 self._rsyncStore.set(iter,
582                                      1, name,
583                                      2, path)
584             self._rsyncCombo.set_active_iter(iter)
585             self._newRsyncTargetDir = path
586
587     def _rsync_combo_changed(self, combobox):
588         newIter = combobox.get_active_iter()
589         if newIter != None:
590             root = self._rsyncStore.get_value(newIter, 2)
591             if root != "Other":
592                 self._newRsyncTargetDir = root
593             else:
594                 msg = _("Select A Back Up Device")
595                 fileDialog = \
596                     gtk.FileChooserDialog(
597                         msg,
598                         self._xml.get_widget("toplevel"),
599                         gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
600                         (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,
601                         gtk.STOCK_OK,gtk.RESPONSE_OK),
602                         None)
603                 self._rsyncCombo.set_sensitive(False)
604                 response = fileDialog.run()
605                 fileDialog.hide()
606                 if response == gtk.RESPONSE_OK:
607                     gFile = fileDialog.get_file()
608                     self._rsync_dev_selected(gFile.get_path())
609                 else:
610                     self._rsync_dev_selected(self._newRsyncTargetDir)
611                 self._rsyncCombo.set_sensitive(True)
612
613     def _rsync_size_warning(self, zpools, zpoolSize,
614                              rsyncTarget, targetSize):
615         # Using decimal "GB" instead of binary "GiB"
616         KB = 1000
617         MB = 1000 * KB
618         GB = 1000 * MB
619         TB = 1000 * GB
620
621         suggestedSize = RSYNCTARGETRATIO * zpoolSize
622         if suggestedSize > TB:
623             sizeStr = "%.1f TB" % round(suggestedSize / float(TB), 1)
624         elif suggestedSize > GB:
625             sizeStr = "%.1f GB" % round(suggestedSize / float(GB), 1)
626         else:
627             sizeStr = "%.1f MB" % round(suggestedSize / float(MB), 1)
628
629         if targetSize > TB:
630             targetStr = "%.1f TB" % round(targetSize / float(TB), 1)
631         elif targetSize > GB:
632             targetStr = "%.1f GB" % round(targetSize / float(GB), 1)
633         else:
634             targetStr = "%.1f MB" % round(targetSize / float(MB), 1)
635
636
637         msg = _("Time Slider suggests a device with a capacity of at "
638                 "least <b>%s</b>.\n"
639                 "The device: \'<b>%s</b>\'\nonly has <b>%s</b>\n"
640                 "Do you want to use it anyway?") \
641                 % (sizeStr, rsyncTarget, targetStr)
642
643         topLevel = self._xml.get_widget("toplevel")
644         dialog = gtk.MessageDialog(topLevel,
645                                    0,
646                                    gtk.MESSAGE_QUESTION,
647                                    gtk.BUTTONS_YES_NO,
648                                    _("Time Slider"))
649         dialog.set_default_response(gtk.RESPONSE_NO)
650         dialog.set_transient_for(topLevel)
651         dialog.set_markup(msg)
652         dialog.set_icon_name("time-slider-setup")
653
654         response = dialog.run()
655         dialog.hide()
656         if response == gtk.RESPONSE_YES:
657             return True
658         else:
659             return False
660
661     def _check_rsync_config(self):
662         """
663            Checks rsync configuration including, filesystem selection,
664            target directory validation and capacity checks.
665            Returns True if everything is OK, otherwise False.
666            Pops up blocking error dialogs to notify users of error
667            conditions before returning.
668         """
669         def _get_mount_point(path):
670             if os.path.ismount(path):
671                 return path
672             else:
673                 return _get_mount_point(abspath(join(path, pardir)))
674
675         if self._rsyncEnabled != True:
676             return True
677
678         if len(self._newRsyncTargetDir) == 0:
679             msg = _("No backup device was selected.\n"
680                     "Please select an empty device.")
681             self._rsync_config_error(msg)
682             return False
683         # There's little that can be done if the device is from a
684         # previous configuration and currently offline. So just 
685         # treat it as being OK based on the assumption that it was
686         # previously deemed to be OK.
687         if self._initialRsyncTargetDir == self._newRsyncTargetDir and \
688            not os.path.exists(self._newRsyncTargetDir):
689             return True
690         # Perform the required validation checks on the
691         # target directory.
692         newTargetDir = self._newRsyncTargetDir
693
694         # We require the whole device. So find the enclosing
695         # mount point and inspect from there.
696         targetMountPoint = abspath(_get_mount_point(newTargetDir))
697
698         # Check that it's writable.
699         f = None
700         testFile = os.path.join(targetMountPoint, ".ts-test")
701         try:
702             f = open(testFile, 'w')
703         except (OSError, IOError):
704             msg = _("\'%s\'\n"
705                     "is not writable. The backup device must "
706                     "be writable by the system administrator." 
707                     "\n\nPlease use a different device.") \
708                     % (targetMountPoint)
709             self._rsync_config_error(msg)
710             return False
711         f.close()
712
713         # Try to create a symlink. Rsync requires this to
714         # do incremental backups and to ensure it's posix like
715         # enough to correctly set file ownerships and perms.
716         os.chdir(targetMountPoint)
717         try:
718             os.link(testFile, ".ts-test-link")
719         except OSError:
720             msg = _("\'%s\'\n"
721                     "contains an incompatible file system. " 
722                     "The selected device must have a Unix "
723                     "style file system that supports file "
724                     "linking, such as UFS"
725                     "\n\nPlease use a different device.") \
726                     % (targetMountPoint)
727             self._rsync_config_error(msg)
728             return False
729         finally:
730             os.unlink(testFile)
731         os.unlink(".ts-test-link")
732
733         # Check that selected directory is either empty
734         # or already preconfigured as a backup target
735         sys,nodeName,rel,ver,arch = os.uname()
736         basePath = os.path.join(targetMountPoint,
737                                 rsyncsmf.RSYNCDIRPREFIX)
738         nodePath = os.path.join(basePath,
739                                 nodeName)
740         configPath = os.path.join(basePath,
741                                     rsyncsmf.RSYNCCONFIGFILE)
742         self._newRsyncTargetSelected = True
743         targetDirKey = None
744
745         contents = os.listdir(targetMountPoint)
746         os.chdir(targetMountPoint)
747
748         # The only other exception to an empty directory is
749         # "lost+found".
750         for item in contents:
751             if (item != rsyncsmf.RSYNCDIRPREFIX and \
752                 item != "lost+found") or \
753                not os.path.isdir(item) or \
754                os.path.islink(item):
755                 msg = _("\'%s\'\n is not an empty device.\n\n"
756                         "Please select an empty device.") \
757                         % (newTargetDir)
758                 self._rsync_config_error(msg)
759                 return False
760
761         # Validate existing directory structure
762         if os.path.exists(basePath):
763             # We only accept a pre-existing directory if
764             # 1. It has a config key that matches that stored by
765             #    the rsync plugin's SMF configuration
766             # 2. It has a single subfolder that matches the nodename
767             #    of this system,
768
769             # Check for previous config key
770             if os.path.exists(configPath):
771                 f = open(configPath, 'r')
772                 for line in f.readlines():
773                     key, val = line.strip().split('=')
774                     if key.strip() == "target_key":
775                         targetDirKey = val.strip()
776                         break
777
778             # Examine anything else in the directory
779             self._targetSelectionError = None
780             dirList = [d for d in os.listdir(basePath) if
781                         d != '.rsync-config']
782             os.chdir(basePath)
783             if len(dirList) > 0:
784                 msg = _("\'%s\'\n is not an empty device.\n\n"
785                         "Please select an empty device.") \
786                         % (newTargetDir)
787                 # No config key or > 1 directory:
788                 # User specified a non empty directory.
789                 if targetDirKey == None or len(dirList) > 1:
790                     self._rsync_config_error(msg)
791                     return False
792                 # Make sure the single item is not a file or symlink.
793                 elif os.path.islink(dirList[0]) or \
794                         os.path.isfile(dirList[0]):
795                     self._rsync_config_error(msg)
796                     return False
797                 else:
798                     # Has 1 other item and a config key. Other
799                     # item must be a directory and must match the
800                     # system nodename and SMF's key value respectively
801                     # respectively
802                     if dirList[0] != nodeName and \
803                         targetDirKey != self._smfTargetKey:
804                         msg = _("\'%s\'\n"
805                                 "is a Time Slider external backup device "
806                                 "that is already in use by another system. "
807                                 "Backup devices may not be shared between "
808                                 "systems." 
809                                 "\n\nPlease use a different device.") \
810                                 % (newTargetDir)
811                         self._rsync_config_error(msg)                                
812                         return False
813                     else:
814                         if dirList[0] == nodeName and \
815                            targetDirKey != self._smfTargetKey:
816                             # Looks like a device that we previously used,
817                             # but discontinued using in favour of some other
818                             # device.
819                             msg = _("\'<b>%s</b>\' appears to be a a device "
820                                     "previously configured for use by this "
821                                     "system.\n\nDo you want resume use of "
822                                     "this device for backups?") \
823                                     % (newTargetDir)
824
825                             topLevel = self._xml.get_widget("toplevel")
826                             dialog = gtk.MessageDialog(topLevel,
827                                                        0,
828                                                        gtk.MESSAGE_QUESTION,
829                                                        gtk.BUTTONS_YES_NO,
830                                                        _("Time Slider"))
831                             dialog.set_default_response(gtk.RESPONSE_NO)
832                             dialog.set_transient_for(topLevel)
833                             dialog.set_markup(msg)
834                             dialog.set_icon_name("time-slider-setup")
835
836                             response = dialog.run()
837                             dialog.hide()
838                             if response == gtk.RESPONSE_NO:
839                                 return False
840                         else:
841                             # Appears to be our own pre-configured directory.
842                             self._newRsyncTargetSelected = False
843
844         # Compare device ID against selected ZFS filesystems
845         # and their enclosing Zpools. The aim is to avoid
846         # a vicous circle caused by backing up snapshots onto
847         # the same pool the snapshots originate from
848         targetDev = os.stat(newTargetDir).st_dev
849         try:
850             fs = self._fsDevices[targetDev]
851             
852             # See if the filesystem itself is selected
853             # and/or any other fileystem on the pool is 
854             # selected.
855             fsEnabled = self._snapStateDic[fs.name]
856             if fsEnabled == True:
857                 # Definitely can't use this since it's a
858                 # snapshotted filesystem.
859                 msg = _("\'%s\'\n"
860                         "belongs to the ZFS filesystem \'%s\' "
861                         "which is already selected for "
862                         "regular ZFS snaphots." 
863                         "\n\nPlease select a drive "
864                         "not already in use by "
865                         "Time Slider") \
866                         % (newTargetDir, fs.name)
867                 self._rsync_config_error(msg)
868                 return False
869             else:
870                 # See if there is anything else on the pool being
871                 # snapshotted
872                 poolName = fs.name.split("/", 1)[0]
873                 for name,mount in self._datasets.list_filesystems():
874                     if name.find(poolName) == 0:
875                         try:
876                             otherEnabled = self._snapStateDic[name]
877                             radioBtn = self._xml.get_widget("defaultfsradio")
878                             snapAll = radioBtn.get_active()
879                             if snapAll or otherEnabled:
880                                 msg = _("\'%s\'\n"
881                                         "belongs to the ZFS pool \'%s\' "
882                                         "which is already being used "
883                                         "to store ZFS snaphots." 
884                                         "\n\nPlease select a drive "
885                                         "not already in use by "
886                                         "Time Slider") \
887                                         % (newTargetDir, poolName)
888                                 self._rsync_config_error(msg)
889                                 return False
890                         except KeyError:
891                             pass               
892         except KeyError:
893             # No match found - good.
894             pass
895
896
897         # Figure out if there's a reasonable amount of free space to
898         # store backups. This is a vague guess at best.
899         allPools = zfs.list_zpools()
900         snapPools = []
901         # FIXME -  this is for custom selection. There is a short
902         # circuit case for default (All) configuration. Don't forget
903         # to implement this short circuit.
904         for poolName in allPools:
905             try:
906                 snapPools.index(poolName)
907             except ValueError:
908                 pool = zfs.ZPool(poolName)
909                 # FIXME - we should include volumes here but they
910                 # can only be set from the command line, not via
911                 # the GUI, so not crucial.
912                 for fsName,mount in pool.list_filesystems():
913                     # Don't try to catch exception. The filesystems
914                     # are already populated in self._snapStateDic
915                     enabled = self._snapStateDic[fsName]
916                     if enabled == True:
917                         snapPools.append(poolName)
918                         break
919
920         sumPoolSize = 0
921         for poolName in snapPools:
922             pool = zfs.ZPool(poolName)
923             # Rough calcualation, but precise enough for
924             # estimation purposes
925             sumPoolSize += pool.get_used_size()
926             sumPoolSize += pool.get_available_size()
927
928
929         # Compare with available space on rsync target dir
930         targetAvail = util.get_available_size(targetMountPoint)
931         targetUsed = util.get_used_size(targetMountPoint)
932         targetSum = targetAvail + targetUsed
933
934         # Recommended Minimum:
935         # At least double the combined size of all pools with
936         # fileystems selected for backup. Variables include,
937         # frequency of data changes, how much efficiency rsync
938         # sacrifices compared to ZFS' block level diff tracking,
939         # whether compression and/or deduplication are enabled 
940         # on the source pool/fileystem.
941         # We don't try to make calculations based on individual
942         # filesystem selection as there are too many unpredictable
943         # variables to make an estimation of any practical use.
944         # Let the user figure that out for themselves.
945
946         # The most consistent measurement is to use the sum of
947         # available and used size on the target fileystem. We
948         # assume based on previous checks that the target device
949         # is only being used for rsync backups and therefore the
950         # used value consists of existing backups and is. Available
951         # space can be reduced for various reasons including the used
952         # value increasing or for nfs mounted zfs fileystems, other
953         # zfs filesystems on the containing pool using up more space.
954         
955
956         targetPoolRatio = targetSum/float(sumPoolSize)
957         if (targetPoolRatio < RSYNCTARGETRATIO):
958             response = self._rsync_size_warning(snapPools,
959                                                  sumPoolSize,
960                                                  targetMountPoint,
961                                                  targetSum)
962             if response == False:
963                 return False
964
965         self._newRsyncTargetDir = targetMountPoint
966         return True
967
968     def _on_ok_clicked(self, widget):
969         # Make sure the dictionaries are empty.
970         self._fsIntentDic = {}
971         self._snapStateDic = {}
972         self._rsyncStateDic = {}
973         enabled = self._xml.get_widget("enablebutton").get_active()
974         self._rsyncEnabled = self._xml.get_widget("rsyncbutton").get_active()
975         if enabled == False:
976             if self._rsyncEnabled == False and \
977                self._initialRsyncState == True:
978                 self._rsyncSMF.disable_service()
979             if self._initialEnabledState == True:
980                 self._sliderSMF.disable_service()
981             # Ignore other changes to the snapshot/rsync configuration
982             # of filesystems. Just broadcast the change and exit.
983             self._configNotify = True
984             self.broadcast_changes()
985             gtk.main_quit()
986         else:
987             model = self._fsTreeView.get_model()
988             snapalldata = self._xml.get_widget("defaultfsradio").get_active()
989                 
990             if snapalldata == True:
991                 model.foreach(self._set_fs_selection_state, True)
992                 if self._rsyncEnabled == True:
993                     model.foreach(self._set_rsync_selection_state, True)
994             else:
995                 model.foreach(self._get_fs_selection_state)
996                 model.foreach(self._get_rsync_selection_state)
997             for fsname in self._snapStateDic:
998                 self._refine_filesys_actions(fsname,
999                                               self._snapStateDic,
1000                                               self._fsIntentDic)
1001                 if self._rsyncEnabled == True:
1002                     self._refine_filesys_actions(fsname,
1003                                                   self._rsyncStateDic,
1004                                                   self._rsyncIntentDic)
1005             if self._rsyncEnabled and \
1006                not self._check_rsync_config():
1007                     return
1008
1009             self._pulseDialog.show()
1010             self._enabler = EnableService(self)
1011             self._enabler.start()
1012             glib.timeout_add(100,
1013                              self._monitor_setup,
1014                              self._xml.get_widget("pulsebar"))
1015
1016     def _on_enablebutton_toggled(self, widget):
1017         expander = self._xml.get_widget("expander")
1018         enabled = widget.get_active()
1019         self._xml.get_widget("filesysframe").set_sensitive(enabled)
1020         expander.set_sensitive(enabled)
1021         if (enabled == False):
1022             expander.set_expanded(False)
1023
1024     def _on_rsyncbutton_toggled(self, widget):
1025         self._rsyncEnabled = widget.get_active()
1026         if self._rsyncEnabled == True:
1027             self._fsTreeView.insert_column(self._rsyncRadioColumn, 1)
1028             self._rsyncCombo.set_sensitive(True)
1029         else:
1030             self._fsTreeView.remove_column(self._rsyncRadioColumn)
1031             self._rsyncCombo.set_sensitive(False)
1032
1033     def _on_defaultfsradio_toggled(self, widget):
1034         if widget.get_active() == True:
1035             self._xml.get_widget("fstreeview").set_sensitive(False)
1036
1037     def _on_selectfsradio_toggled(self, widget):
1038        if widget.get_active() == True:
1039             self._xml.get_widget("fstreeview").set_sensitive(True)
1040
1041     def _advancedbox_unmap(self, widget):
1042         # Auto shrink the window by subtracting the frame's height
1043         # requistion from the window's height requisition
1044         myrequest = widget.size_request()
1045         toplevel = self._xml.get_widget("toplevel")
1046         toprequest = toplevel.size_request()
1047         toplevel.resize(toprequest[0], toprequest[1] - myrequest[1])
1048
1049     def _get_fs_selection_state(self, model, path, iter):
1050         fsname = self._fsListStore.get_value(iter, 3)    
1051         enabled = self._fsListStore.get_value(iter, 0)
1052         self._snapStateDic[fsname] = enabled
1053
1054     def _get_rsync_selection_state(self, model, path, iter):
1055         fsname = self._fsListStore.get_value(iter, 3)
1056         enabled = self._fsListStore.get_value(iter, 1)
1057         self._rsyncStateDic[fsname] = enabled
1058
1059     def _set_fs_selection_state(self, model, path, iter, selected):
1060         fsname = self._fsListStore.get_value(iter, 3)
1061         self._snapStateDic[fsname] = selected
1062
1063     def _set_rsync_selection_state(self, model, path, iter, selected):
1064         fsname = self._fsListStore.get_value(iter, 3)
1065         self._rsyncStateDic[fsname] = selected
1066
1067     def _refine_filesys_actions(self, fsname, inputdic, actions):
1068         selected = inputdic[fsname]
1069         try:
1070             fstag = actions[fsname]
1071             # Found so we can skip over.
1072         except KeyError:
1073             # Need to check parent value to see if
1074             # we should set explicitly or just inherit.
1075             path = fsname.rsplit("/", 1)
1076             parentName = path[0]
1077             if parentName == fsname:
1078                 # Means this filesystem is the root of the pool
1079                 # so we need to set it explicitly.
1080                 actions[fsname] = \
1081                     FilesystemIntention(fsname, selected, False)
1082             else:
1083                 parentIntent = None
1084                 inherit = False
1085                 # Check if parent is already set and if so whether to
1086                 # inherit or override with a locally set property value.
1087                 try:
1088                     # Parent has already been registered
1089                     parentIntent = actions[parentName]
1090                 except:
1091                     # Parent not yet set, so do that recursively to figure
1092                     # out if we need to inherit or set a local property on
1093                     # this child filesystem.
1094                     self._refine_filesys_actions(parentName,
1095                                                   inputdic,
1096                                                   actions)
1097                     parentIntent = actions[parentName]
1098                 if parentIntent.selected == selected:
1099                     inherit = True
1100                 actions[fsname] = \
1101                     FilesystemIntention(fsname, selected, inherit)
1102
1103     def _validate_rsync_target(self, path):
1104         """
1105             Tests path to see if it is the pre-configured
1106             rsync backup device path.
1107             Returns True on success, otherwise False
1108         """
1109         # FIXME - this is duplicate in applet.py and rsync-backup.py
1110         # It should be moved into a shared module
1111         if not os.path.exists(path):
1112             return False
1113         testDir = os.path.join(path,
1114                                 rsyncsmf.RSYNCDIRPREFIX,
1115                                 self._nodeName)
1116         testKeyFile = os.path.join(path,
1117                                     rsyncsmf.RSYNCDIRPREFIX,
1118                                     rsyncsmf.RSYNCCONFIGFILE)
1119         if os.path.exists(testDir) and \
1120             os.path.exists(testKeyFile):
1121             testKeyVal = None
1122             f = open(testKeyFile, 'r')
1123             for line in f.readlines():
1124                 key, val = line.strip().split('=')
1125                 if key.strip() == "target_key":
1126                     targetKey = val.strip()
1127                     break
1128             f.close()
1129             if targetKey == self._smfTargetKey:
1130                 return True
1131         return False
1132
1133
1134     def commit_filesystem_selection(self):
1135         """
1136         Commits the intended filesystem selection actions based on the
1137         user's UI configuration to disk. Compares with initial startup
1138         configuration and applies the minimum set of necessary changes.
1139         """
1140         for fsname,fsmountpoint in self._datasets.list_filesystems():
1141             fs = zfs.Filesystem(fsname, fsmountpoint)
1142             try:
1143                 initialIntent = self._initialFsIntentDic[fsname]
1144                 intent = self._fsIntentDic[fsname]
1145                 if intent == initialIntent:
1146                     continue
1147                 fs.set_auto_snap(intent.selected, intent.inherited)
1148
1149             except KeyError:
1150                 pass
1151
1152     def commit_rsync_selection(self):
1153         """
1154         Commits the intended filesystem selection actions based on the
1155         user's UI configuration to disk. Compares with initial startup
1156         configuration and applies the minimum set of necessary changes.
1157         """
1158         for fsname,fsmountpoint in self._datasets.list_filesystems():
1159             fs = zfs.Filesystem(fsname, fsmountpoint)
1160             try:
1161                 initialIntent = self._initialRsyncIntentDic[fsname]
1162                 intent = self._rsyncIntentDic[fsname]
1163                 if intent == initialIntent:
1164                     continue
1165                 if intent.inherited == True and \
1166                     initialIntent.inherited == False:
1167                     fs.unset_user_property(rsyncsmf.RSYNCFSTAG)
1168                 else:
1169                     if intent.selected == True:
1170                         value = "true"
1171                     else:
1172                         value = "false"
1173                     fs.set_user_property(rsyncsmf.RSYNCFSTAG,
1174                                          value)
1175             except KeyError:
1176                 pass
1177
1178     def setup_rsync_config(self):
1179         if self._rsyncEnabled == True:
1180             if self._newRsyncTargetSelected == True:
1181                 sys,nodeName,rel,ver,arch = os.uname()
1182                 basePath = os.path.join(self._newRsyncTargetDir,
1183                                         rsyncsmf.RSYNCDIRPREFIX,)
1184                 nodePath = os.path.join(basePath,
1185                                         nodeName)
1186                 configPath = os.path.join(basePath,
1187                                           rsyncsmf.RSYNCCONFIGFILE)
1188                 newKey = generate_random_key()
1189                 try:
1190                     origmask = os.umask(0222)
1191                     if not os.path.exists(nodePath):
1192                         os.makedirs(nodePath, 0755)
1193                     f = open(configPath, 'w')
1194                     f.write("target_key=%s\n" % (newKey))
1195                     f.close()
1196                     os.umask(origmask)
1197                 except OSError as e:
1198                     self._pulseDialog.hide()
1199                     sys.stderr.write("Error configuring external " \
1200                                      "backup device:\n" \
1201                                      "%s\n\nReason:\n %s") \
1202                                      % (self._newRsyncTargetDir, str(e))
1203                     sys.exit(-1)
1204                 self._rsyncSMF.set_target_dir(self._newRsyncTargetDir)
1205                 self._rsyncSMF.set_target_key(newKey)
1206                 # Applet monitors rsyncTargetDir so make sure to notify it.
1207                 self._configNotify = True
1208         return
1209
1210     def setup_services(self):
1211         # Take care of the rsync plugin service first since time-slider
1212         # will query it.
1213         # Changes to rsync or time-slider SMF service State should be
1214         # broadcast to let notification applet refresh.
1215         if self._rsyncEnabled == True and \
1216             self._initialRsyncState == False:
1217             self._rsyncSMF.enable_service()
1218             self._configNotify = True
1219         elif self._rsyncEnabled == False and \
1220             self._initialRsyncState == True:
1221             self._rsyncSMF.disable_service()
1222             self._configNotify = True
1223         customSelection = self._xml.get_widget("selectfsradio").get_active()
1224         if customSelection != self._initialCustomSelection:
1225             self._sliderSMF.set_custom_selection(customSelection)
1226         if self._initialEnabledState == False:
1227             enable_default_schedules()
1228             self._sliderSMF.enable_service()
1229             self._configNotify = True
1230
1231     def set_cleanup_level(self):
1232         """
1233         Wrapper function to set the warning level cleanup threshold
1234         value as a percentage of pool capacity.
1235         """
1236         level = self._xml.get_widget("capspinbutton").get_value_as_int()
1237         if level != self._initialCleanupLevel:
1238             self._sliderSMF.set_cleanup_level("warning", level)
1239
1240     def broadcast_changes(self):
1241         """
1242         Blunt instrument to notify D-Bus listeners such as notification
1243         applet to rescan service configuration
1244         """
1245         if self._configNotify == False:
1246             return
1247         self._dbus.config_changed()
1248
1249     def _on_deletesnapshots_clicked(self, widget):
1250         cmdpath = os.path.join(os.path.dirname(self._execPath), \
1251                                "../lib/time-slider-delete")
1252         p = subprocess.Popen(cmdpath, close_fds=True)
1253
1254
1255 class EnableService(threading.Thread):
1256
1257     def __init__(self, setupManager):
1258         threading.Thread.__init__(self)
1259         self._setupManager = setupManager
1260
1261     def run(self):
1262         try:
1263             # Set the service state last so that the ZFS filesystems
1264             # are correctly tagged before the snapshot scripts check them
1265             self._setupManager.commit_filesystem_selection()
1266             self._setupManager.commit_rsync_selection()
1267             self._setupManager.set_cleanup_level()
1268             self._setupManager.setup_rsync_config()
1269             self._setupManager.setup_services()
1270             self._setupManager.broadcast_changes()
1271         except RuntimeError, message:
1272             sys.stderr.write(str(message))
1273
1274 def generate_random_key(length=32):
1275     """
1276     Returns a 'length' byte character composed of random letters and
1277     unsigned single digit integers. Used to create a random
1278     signature key to identify pre-configured backup directories
1279     for the rsync plugin
1280     """
1281     from string import letters, digits
1282     from random import choice
1283     return ''.join([choice(letters + digits) \
1284               for i in range(length)])
1285
1286 def main(argv):
1287     rbacp = RBACprofile()
1288     # The setup GUI needs to be run as root in order to ensure
1289     # that the rsync backup target directory is accessible by
1290     # root and to perform validation checks on it.
1291     # This GUI can be launched with an euid of root in one of
1292     # the following 3 ways;
1293     # 0. Run by the superuser (root)
1294     # 1. Run via gksu to allow a non priviliged user to authenticate
1295     #    as the superuser (root)
1296
1297     if os.geteuid() == 0:
1298         manager = SetupManager(argv)
1299         gtk.gdk.threads_enter()
1300         gtk.main()
1301         gtk.gdk.threads_leave()
1302     elif os.path.exists(argv) and os.path.exists("/usr/bin/gksu"):
1303         # Run via gksu, which will prompt for the root password
1304         os.unsetenv("DBUS_SESSION_BUS_ADDRESS")
1305         os.execl("/usr/bin/gksu", "gksu", argv)
1306         # Shouldn't reach this point
1307         sys.exit(1)
1308     else:
1309         dialog = gtk.MessageDialog(None,
1310                                    0,
1311                                    gtk.MESSAGE_ERROR,
1312                                    gtk.BUTTONS_CLOSE,
1313                                    _("Insufficient Priviliges"))
1314         dialog.format_secondary_text(_("The snapshot manager service requires "
1315                                        "administrative privileges to run. "
1316                                        "You have not been assigned the necessary"
1317                                        "administrative priviliges."
1318                                        "\n\nConsult your system administrator "))
1319         dialog.set_icon_name("time-slider-setup")
1320         dialog.run()
1321         sys.exit(1)
1322