X-Git-Url: https://git.camperquake.de/gitweb.cgi?p=time-slider.git;a=blobdiff_plain;f=usr%2Fshare%2Ftime-slider%2Flib%2Ftime_slider%2Fsetupgui.py~;fp=usr%2Fshare%2Ftime-slider%2Flib%2Ftime_slider%2Fsetupgui.py~;h=0000000000000000000000000000000000000000;hp=b3a37b6d900ec13f10ca9988c85cbeea95e23638;hb=1af5ab1ed1c0a7bade946f5e257d6512cc846c27;hpb=d021224696e8524cab857c27370e33aeaf024130 diff --git a/usr/share/time-slider/lib/time_slider/setupgui.py~ b/usr/share/time-slider/lib/time_slider/setupgui.py~ deleted file mode 100755 index b3a37b6..0000000 --- a/usr/share/time-slider/lib/time_slider/setupgui.py~ +++ /dev/null @@ -1,1322 +0,0 @@ -#!/usr/bin/python2.6 -# -# CDDL HEADER START -# -# The contents of this file are subject to the terms of the -# Common Development and Distribution License (the "License"). -# You may not use this file except in compliance with the License. -# -# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE -# or http://www.opensolaris.org/os/licensing. -# See the License for the specific language governing permissions -# and limitations under the License. -# -# When distributing Covered Code, include this CDDL HEADER in each -# file and include the License file at usr/src/OPENSOLARIS.LICENSE. -# If applicable, add the following below this CDDL HEADER, with the -# fields enclosed by brackets "[]" replaced with your own identifying -# information: Portions Copyright [yyyy] [name of copyright owner] -# -# CDDL HEADER END -# - -import sys -import os -import subprocess -import threading -import util -import smf -from autosnapsmf import enable_default_schedules, disable_default_schedules - -from os.path import abspath, dirname, join, pardir -sys.path.insert(0, join(dirname(__file__), pardir, "plugin")) -import plugin -sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync")) -import rsyncsmf - -try: - import pygtk - pygtk.require("2.4") -except: - pass -try: - import gtk - import gtk.glade - gtk.gdk.threads_init() -except: - sys.exit(1) - -import glib -import gobject -import gio -import dbus -import dbus.service -import dbus.mainloop -import dbus.mainloop.glib -import dbussvc - - -# This is the rough guess ratio used for rsync backup device size -# vs. the total size of the pools it's expected to backup. -RSYNCTARGETRATIO = 2 - -# here we define the path constants so that other modules can use it. -# this allows us to get access to the shared files without having to -# know the actual location, we just use the location of the current -# file and use paths relative to that. -SHARED_FILES = os.path.abspath(os.path.join(os.path.dirname(__file__), - os.path.pardir, - os.path.pardir)) -LOCALE_PATH = os.path.join('/usr', 'share', 'locale') -RESOURCE_PATH = os.path.join(SHARED_FILES, 'res') - -# the name of the gettext domain. because we have our translation files -# not in a global folder this doesn't really matter, setting it to the -# application name is a good idea tough. -GETTEXT_DOMAIN = 'time-slider' - -# set up the glade gettext system and locales -gtk.glade.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH) -gtk.glade.textdomain(GETTEXT_DOMAIN) - -import zfs -from timeslidersmf import TimeSliderSMF -from rbac import RBACprofile - - -class FilesystemIntention: - - def __init__(self, name, selected, inherited): - self.name = name - self.selected = selected - self.inherited = inherited - - def __str__(self): - return_string = "Filesystem name: " + self.name + \ - "\n\tSelected: " + str(self.selected) + \ - "\n\tInherited: " + str(self.inherited) - return return_string - - def __eq__(self, other): - if self.name != other.name: - return False - if self.inherited and other.inherited: - return True - elif not self.inherited and other.inherited: - return False - if (self.selected == other.selected) and \ - (self.inherited == other.inherited): - return True - else: - return False - -class SetupManager: - - def __init__(self, execpath): - self._execPath = execpath - self._datasets = zfs.Datasets() - self._xml = gtk.glade.XML("%s/../../glade/time-slider-setup.glade" \ - % (os.path.dirname(__file__))) - - # Tell dbus to use the gobject mainloop for async ops - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - dbus.mainloop.glib.threads_init() - - # Register a bus name with the system dbus daemon - systemBus = dbus.SystemBus() - busName = dbus.service.BusName("org.opensolaris.TimeSlider.config", - systemBus) - self._dbus = dbussvc.Config(systemBus, - '/org/opensolaris/TimeSlider/config') - # Used later to trigger a D-Bus notification of select configuration - # changes made - self._configNotify = False - - # These variables record the initial UI state which are used - # later to compare against the UI state when the OK or Cancel - # button is clicked and apply the minimum set of necessary - # configuration changes. Prevents minor changes taking ages - # to be applied by the GUI. - self._initialEnabledState = None - self._initialRsyncState = None - self._initialRsyncTargetDir = None - self._initialCleanupLevel = None - self._initialCustomSelection = False - self._initialSnapStateDic = {} - self._initialRsyncStateDic = {} - self._initialFsIntentDic = {} - self._initialRsyncIntentDic = {} - - # Currently selected rsync backup device via the GUI. - self._newRsyncTargetDir = None - # Used to store GUI filesystem selection state and the - # set of intended properties to apply to zfs filesystems. - self._snapStateDic = {} - self._rsyncStateDic = {} - self._fsIntentDic = {} - self._rsyncIntentDic = {} - # Dictionary that maps device ID numbers to zfs filesystem objects - self._fsDevices = {} - - topLevel = self._xml.get_widget("toplevel") - self._pulseDialog = self._xml.get_widget("pulsedialog") - self._pulseDialog.set_transient_for(topLevel) - - # gio.VolumeMonitor reference - self._vm = gio.volume_monitor_get() - self._vm.connect("mount-added", self._mount_added) - self._vm.connect("mount-removed" , self._mount_removed) - - self._fsListStore = gtk.ListStore(bool, - bool, - str, - str, - gobject.TYPE_PYOBJECT) - filesystems = self._datasets.list_filesystems() - for fsname,fsmountpoint in filesystems: - if (fsmountpoint == "legacy"): - mountpoint = _("Legacy") - else: - mountpoint = fsmountpoint - fs = zfs.Filesystem(fsname, fsmountpoint) - # Note that we don't deal with legacy mountpoints. - if fsmountpoint != "legacy" and fs.is_mounted(): - self._fsDevices[os.stat(fsmountpoint).st_dev] = fs - snap = fs.get_auto_snap() - rsyncstr = fs.get_user_property(rsyncsmf.RSYNCFSTAG) - if rsyncstr == "true": - rsync = True - else: - rsync = False - # Rsync is only performed on snapshotted filesystems. - # So treat as False if rsync is set to true independently - self._fsListStore.append([snap, snap & rsync, - mountpoint, fs.name, fs]) - self._initialSnapStateDic[fs.name] = snap - self._initialRsyncStateDic[fs.name] = snap & rsync - del filesystems - - for fsname in self._initialSnapStateDic: - self._refine_filesys_actions(fsname, - self._initialSnapStateDic, - self._initialFsIntentDic) - self._refine_filesys_actions(fsname, - self._initialRsyncStateDic, - self._initialRsyncIntentDic) - - self._fsTreeView = self._xml.get_widget("fstreeview") - self._fsTreeView.set_sensitive(False) - self._fsTreeView.set_size_request(10, 200) - - self._fsTreeView.set_model(self._fsListStore) - - cell0 = gtk.CellRendererToggle() - cell1 = gtk.CellRendererToggle() - cell2 = gtk.CellRendererText() - cell3 = gtk.CellRendererText() - - radioColumn = gtk.TreeViewColumn(_("Select"), - cell0, active=0) - self._fsTreeView.insert_column(radioColumn, 0) - - self._rsyncRadioColumn = gtk.TreeViewColumn(_("Replicate"), - cell1, active=1) - nameColumn = gtk.TreeViewColumn(_("Mount Point"), - cell2, text=2) - self._fsTreeView.insert_column(nameColumn, 2) - mountPointColumn = gtk.TreeViewColumn(_("File System Name"), - cell3, text=3) - self._fsTreeView.insert_column(mountPointColumn, 3) - cell0.connect('toggled', self._row_toggled) - cell1.connect('toggled', self._rsync_cell_toggled) - advancedBox = self._xml.get_widget("advancedbox") - advancedBox.connect('unmap', self._advancedbox_unmap) - - self._rsyncSMF = rsyncsmf.RsyncSMF("%s:rsync" \ - %(plugin.PLUGINBASEFMRI)) - state = self._rsyncSMF.get_service_state() - self._initialRsyncTargetDir = self._rsyncSMF.get_target_dir() - # Check for the default, unset value of "" from SMF. - if self._initialRsyncTargetDir == '""': - self._initialRsyncTargetDir = '' - self._newRsyncTargetDir = self._initialRsyncTargetDir - self._smfTargetKey = self._rsyncSMF.get_target_key() - self._newRsyncTargetSelected = False - sys,self._nodeName,rel,ver,arch = os.uname() - - # Model columns: - # 0 Themed icon list (python list) - # 1 device root - # 2 volume name - # 3 Is gio.Mount device - # 4 Is separator (for comboBox separator rendering) - self._rsyncStore = gtk.ListStore(gobject.TYPE_PYOBJECT, - gobject.TYPE_STRING, - gobject.TYPE_STRING, - gobject.TYPE_BOOLEAN, - gobject.TYPE_BOOLEAN) - self._rsyncCombo = self._xml.get_widget("rsyncdevcombo") - mounts = self._vm.get_mounts() - for mount in mounts: - self._mount_added(self._vm, mount) - if len(mounts) > 0: - # Add a separator - self._rsyncStore.append((None, None, None, None, True)) - del mounts - - if len(self._newRsyncTargetDir) == 0: - self._rsyncStore.append((['folder'], - _("(None)"), - '', - False, - False)) - # Add a separator - self._rsyncStore.append((None, None, None, None, True)) - self._rsyncStore.append((None, _("Other..."), "Other", False, False)) - self._iconCell = gtk.CellRendererPixbuf() - self._nameCell = gtk.CellRendererText() - self._rsyncCombo.clear() - self._rsyncCombo.pack_start(self._iconCell, False) - self._rsyncCombo.set_cell_data_func(self._iconCell, - self._icon_cell_render) - self._rsyncCombo.pack_end(self._nameCell) - self._rsyncCombo.set_attributes(self._nameCell, text=1) - self._rsyncCombo.set_row_separator_func(self._row_separator) - self._rsyncCombo.set_model(self._rsyncStore) - self._rsyncCombo.connect("changed", self._rsync_combo_changed) - # Force selection of currently configured device - self._rsync_dev_selected(self._newRsyncTargetDir) - - # signal dictionary - dic = {"on_ok_clicked" : self._on_ok_clicked, - "on_cancel_clicked" : gtk.main_quit, - "on_snapshotmanager_delete_event" : gtk.main_quit, - "on_enablebutton_toggled" : self._on_enablebutton_toggled, - "on_rsyncbutton_toggled" : self._on_rsyncbutton_toggled, - "on_defaultfsradio_toggled" : self._on_defaultfsradio_toggled, - "on_selectfsradio_toggled" : self._on_selectfsradio_toggled, - "on_deletesnapshots_clicked" : self._on_deletesnapshots_clicked} - self._xml.signal_autoconnect(dic) - - if state != "disabled": - self._rsyncEnabled = True - self._xml.get_widget("rsyncbutton").set_active(True) - self._initialRsyncState = True - else: - self._rsyncEnabled = False - self._rsyncCombo.set_sensitive(False) - self._initialRsyncState = False - - # Initialise SMF service instance state. - try: - self._sliderSMF = TimeSliderSMF() - except RuntimeError,message: - self._xml.get_widget("toplevel").set_sensitive(False) - dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"), - 0, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_CLOSE, - _("Snapshot manager service error")) - dialog.format_secondary_text(_("The snapshot manager service does " - "not appear to be installed on this " - "system." - "\n\nSee the svcs(1) man page for more " - "information." - "\n\nDetails:\n%s")%(message)) - dialog.set_icon_name("time-slider-setup") - dialog.run() - sys.exit(1) - - if self._sliderSMF.svcstate == "disabled": - self._xml.get_widget("enablebutton").set_active(False) - self._initialEnabledState = False - elif self._sliderSMF.svcstate == "offline": - self._xml.get_widget("toplevel").set_sensitive(False) - errors = ''.join("%s\n" % (error) for error in \ - self._sliderSMF.find_dependency_errors()) - dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"), - 0, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_CLOSE, - _("Snapshot manager service dependency error")) - dialog.format_secondary_text(_("The snapshot manager service has " - "been placed offline due to a dependency " - "problem. The following dependency problems " - "were found:\n\n%s\n\nRun \"svcs -xv\" from " - "a command prompt for more information about " - "these dependency problems.") % errors) - dialog.set_icon_name("time-slider-setup") - dialog.run() - sys.exit(1) - elif self._sliderSMF.svcstate == "maintenance": - self._xml.get_widget("toplevel").set_sensitive(False) - dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"), - 0, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_CLOSE, - _("Snapshot manager service error")) - dialog.format_secondary_text(_("The snapshot manager service has " - "encountered a problem and has been " - "disabled until the problem is fixed." - "\n\nSee the svcs(1) man page for more " - "information.")) - dialog.set_icon_name("time-slider-setup") - dialog.run() - sys.exit(1) - else: - # FIXME: Check transitional states - self._xml.get_widget("enablebutton").set_active(True) - self._initialEnabledState = True - - - # Emit a toggled signal so that the initial GUI state is consistent - self._xml.get_widget("enablebutton").emit("toggled") - # Check the snapshotting policy (UserData (default), or Custom) - self._initialCustomSelection = self._sliderSMF.is_custom_selection() - if self._initialCustomSelection == True: - self._xml.get_widget("selectfsradio").set_active(True) - # Show the advanced controls so the user can see the - # customised configuration. - if self._sliderSMF.svcstate != "disabled": - self._xml.get_widget("expander").set_expanded(True) - else: # "false" or any other non "true" value - self._xml.get_widget("defaultfsradio").set_active(True) - - # Set the cleanup threshhold value - spinButton = self._xml.get_widget("capspinbutton") - critLevel = self._sliderSMF.get_cleanup_level("critical") - warnLevel = self._sliderSMF.get_cleanup_level("warning") - - # Force the warning level to something practical - # on the lower end, and make it no greater than the - # critical level specified in the SVC instance. - spinButton.set_range(70, critLevel) - self._initialCleanupLevel = warnLevel - if warnLevel > 70: - spinButton.set_value(warnLevel) - else: - spinButton.set_value(70) - - def _icon_cell_render(self, celllayout, cell, model, iter): - iconList = self._rsyncStore.get_value(iter, 0) - if iconList != None: - gicon = gio.ThemedIcon(iconList) - cell.set_property("gicon", gicon) - else: - root = self._rsyncStore.get_value(iter, 2) - if root == "Other": - cell.set_property("gicon", None) - - def _row_separator(self, model, iter): - return model.get_value(iter, 4) - - def _mount_added(self, volume_monitor, mount): - icon = mount.get_icon() - iconList = icon.get_names() - if iconList == None: - iconList = ['drive-harddisk', 'drive'] - root = mount.get_root() - path = root.get_path() - mountName = mount.get_name() - volume = mount.get_volume() - if volume == None: - volName = mount.get_name() - if volName == None: - volName = os.path.split(path)[1] - else: - volName = volume.get_name() - - # Check to see if there is at least one gio.Mount device already - # in the ListStore. If not, then we also need to add a separator - # row. - iter = self._rsyncStore.get_iter_first() - if iter and self._rsyncStore.get_value(iter, 3) == False: - self._rsyncStore.insert(0, (None, None, None, None, True)) - - self._rsyncStore.insert(0, (iconList, volName, path, True, False)) - # If this happens to be the already configured backup device - # and the user hasn't tried to change device yet, auto select - # it. - if self._initialRsyncTargetDir == self._newRsyncTargetDir: - if self._validate_rsync_target(path) == True: - self._rsyncCombo.set_active(0) - - def _mount_removed(self, volume_monitor, mount): - root = mount.get_root() - path = root.get_path() - iter = self._rsyncStore.get_iter_first() - mountIter = None - numMounts = 0 - # Search gio.Mount devices - while iter != None and \ - self._rsyncStore.get_value(iter, 3) == True: - numMounts += 1 - compPath = self._rsyncStore.get_value(iter, 2) - if compPath == path: - mountIter = iter - break - else: - iter = self._rsyncStore.iter_next(iter) - if mountIter != None: - if numMounts == 1: - # Need to remove the separator also since - # there will be no more gio.Mount devices - # shown in the combo box - sepIter = self._rsyncStore.iter_next(mountIter) - if self._rsyncStore.get_value(sepIter, 4) == True: - self._rsyncStore.remove(sepIter) - self._rsyncStore.remove(mountIter) - iter = self._rsyncStore.get_iter_first() - # Insert a custom folder if none exists already - if self._rsyncStore.get_value(iter, 2) == "Other": - path = self._initialRsyncTargetDir - length = len(path) - if length > 1: - name = os.path.split(path)[1] - elif length == 1: - name = path - else: # Indicates path is unset: '' - name = _("(None)") - iter = self._rsyncStore.insert_before(iter, - (None, - None, - None, - None, - True)) - iter = self._rsyncStore.insert_before(iter, - (['folder'], - name, - path, - False, - False)) - self._rsyncCombo.set_active_iter(iter) - - def _monitor_setup(self, pulseBar): - if self._enabler.isAlive() == True: - pulseBar.pulse() - return True - else: - gtk.main_quit() - - def _row_toggled(self, renderer, path): - model = self._fsTreeView.get_model() - iter = model.get_iter(path) - state = renderer.get_active() - if state == False: - self._fsListStore.set_value(iter, 0, True) - else: - self._fsListStore.set_value(iter, 0, False) - self._fsListStore.set_value(iter, 1, False) - - def _rsync_cell_toggled(self, renderer, path): - model = self._fsTreeView.get_model() - iter = model.get_iter(path) - state = renderer.get_active() - rowstate = self._fsListStore.get_value(iter, 0) - if rowstate == True: - if state == False: - self._fsListStore.set_value(iter, 1, True) - else: - self._fsListStore.set_value(iter, 1, False) - - def _rsync_config_error(self, msg): - topLevel = self._xml.get_widget("toplevel") - dialog = gtk.MessageDialog(topLevel, - 0, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_CLOSE, - _("Unsuitable Backup Location")) - dialog.format_secondary_text(msg) - dialog.set_icon_name("time-slider-setup") - dialog.run() - dialog.hide() - return - - def _rsync_dev_selected(self, path): - iter = self._rsyncStore.get_iter_first() - while iter != None: - # Break out when we hit a non gio.Mount device - if self._rsyncStore.get_value(iter, 3) == False: - break - compPath = self._rsyncStore.get_value(iter, 2) - if compPath == path: - self._rsyncCombo.set_active_iter(iter) - self._newRsyncTargetDir = path - return - else: - iter = self._rsyncStore.iter_next(iter) - - # Not one of the shortcut RMM devices, so it's - # some other path on the filesystem. - # iter may be pointing at a separator. Increment - # to next row iter if so. - if self._rsyncStore.get_value(iter, 4) == True: - iter = self._rsyncStore.iter_next(iter) - - if iter != None: - if len(path) > 1: - name = os.path.split(path)[1] - elif len(path) == 1: - name = path - else: # Indicates path is unset: '' - name = _("(None)") - # Could be either the custom folder selection - # row or the "Other" row if the custom row - # was not created. If "Other" then create the - # custom row and separator now at this position - if self._rsyncStore.get_value(iter, 2) == "Other": - iter = self._rsyncStore.insert_before(iter, - (None, - None, - None, - None, - True)) - iter = self._rsyncStore.insert_before(iter, - (['folder'], - name, - path, - False, - False)) - else: - self._rsyncStore.set(iter, - 1, name, - 2, path) - self._rsyncCombo.set_active_iter(iter) - self._newRsyncTargetDir = path - - def _rsync_combo_changed(self, combobox): - newIter = combobox.get_active_iter() - if newIter != None: - root = self._rsyncStore.get_value(newIter, 2) - if root != "Other": - self._newRsyncTargetDir = root - else: - msg = _("Select A Back Up Device") - fileDialog = \ - gtk.FileChooserDialog( - msg, - self._xml.get_widget("toplevel"), - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, - (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL, - gtk.STOCK_OK,gtk.RESPONSE_OK), - None) - self._rsyncCombo.set_sensitive(False) - response = fileDialog.run() - fileDialog.hide() - if response == gtk.RESPONSE_OK: - gFile = fileDialog.get_file() - self._rsync_dev_selected(gFile.get_path()) - else: - self._rsync_dev_selected(self._newRsyncTargetDir) - self._rsyncCombo.set_sensitive(True) - - def _rsync_size_warning(self, zpools, zpoolSize, - rsyncTarget, targetSize): - # Using decimal "GB" instead of binary "GiB" - KB = 1000 - MB = 1000 * KB - GB = 1000 * MB - TB = 1000 * GB - - suggestedSize = RSYNCTARGETRATIO * zpoolSize - if suggestedSize > TB: - sizeStr = "%.1f TB" % round(suggestedSize / float(TB), 1) - elif suggestedSize > GB: - sizeStr = "%.1f GB" % round(suggestedSize / float(GB), 1) - else: - sizeStr = "%.1f MB" % round(suggestedSize / float(MB), 1) - - if targetSize > TB: - targetStr = "%.1f TB" % round(targetSize / float(TB), 1) - elif targetSize > GB: - targetStr = "%.1f GB" % round(targetSize / float(GB), 1) - else: - targetStr = "%.1f MB" % round(targetSize / float(MB), 1) - - - msg = _("Time Slider suggests a device with a capacity of at " - "least %s.\n" - "The device: \'%s\'\nonly has %s\n" - "Do you want to use it anyway?") \ - % (sizeStr, rsyncTarget, targetStr) - - topLevel = self._xml.get_widget("toplevel") - dialog = gtk.MessageDialog(topLevel, - 0, - gtk.MESSAGE_QUESTION, - gtk.BUTTONS_YES_NO, - _("Time Slider")) - dialog.set_default_response(gtk.RESPONSE_NO) - dialog.set_transient_for(topLevel) - dialog.set_markup(msg) - dialog.set_icon_name("time-slider-setup") - - response = dialog.run() - dialog.hide() - if response == gtk.RESPONSE_YES: - return True - else: - return False - - def _check_rsync_config(self): - """ - Checks rsync configuration including, filesystem selection, - target directory validation and capacity checks. - Returns True if everything is OK, otherwise False. - Pops up blocking error dialogs to notify users of error - conditions before returning. - """ - def _get_mount_point(path): - if os.path.ismount(path): - return path - else: - return _get_mount_point(abspath(join(path, pardir))) - - if self._rsyncEnabled != True: - return True - - if len(self._newRsyncTargetDir) == 0: - msg = _("No backup device was selected.\n" - "Please select an empty device.") - self._rsync_config_error(msg) - return False - # There's little that can be done if the device is from a - # previous configuration and currently offline. So just - # treat it as being OK based on the assumption that it was - # previously deemed to be OK. - if self._initialRsyncTargetDir == self._newRsyncTargetDir and \ - not os.path.exists(self._newRsyncTargetDir): - return True - # Perform the required validation checks on the - # target directory. - newTargetDir = self._newRsyncTargetDir - - # We require the whole device. So find the enclosing - # mount point and inspect from there. - targetMountPoint = abspath(_get_mount_point(newTargetDir)) - - # Check that it's writable. - f = None - testFile = os.path.join(targetMountPoint, ".ts-test") - try: - f = open(testFile, 'w') - except (OSError, IOError): - msg = _("\'%s\'\n" - "is not writable. The backup device must " - "be writable by the system administrator." - "\n\nPlease use a different device.") \ - % (targetMountPoint) - self._rsync_config_error(msg) - return False - f.close() - - # Try to create a symlink. Rsync requires this to - # do incremental backups and to ensure it's posix like - # enough to correctly set file ownerships and perms. - os.chdir(targetMountPoint) - try: - os.link(testFile, ".ts-test-link") - except OSError: - msg = _("\'%s\'\n" - "contains an incompatible file system. " - "The selected device must have a Unix " - "style file system that supports file " - "linking, such as UFS" - "\n\nPlease use a different device.") \ - % (targetMountPoint) - self._rsync_config_error(msg) - return False - finally: - os.unlink(testFile) - os.unlink(".ts-test-link") - - # Check that selected directory is either empty - # or already preconfigured as a backup target - sys,nodeName,rel,ver,arch = os.uname() - basePath = os.path.join(targetMountPoint, - rsyncsmf.RSYNCDIRPREFIX) - nodePath = os.path.join(basePath, - nodeName) - configPath = os.path.join(basePath, - rsyncsmf.RSYNCCONFIGFILE) - self._newRsyncTargetSelected = True - targetDirKey = None - - contents = os.listdir(targetMountPoint) - os.chdir(targetMountPoint) - - # The only other exception to an empty directory is - # "lost+found". - for item in contents: - if (item != rsyncsmf.RSYNCDIRPREFIX and \ - item != "lost+found") or \ - not os.path.isdir(item) or \ - os.path.islink(item): - msg = _("\'%s\'\n is not an empty device.\n\n" - "Please select an empty device.") \ - % (newTargetDir) - self._rsync_config_error(msg) - return False - - # Validate existing directory structure - if os.path.exists(basePath): - # We only accept a pre-existing directory if - # 1. It has a config key that matches that stored by - # the rsync plugin's SMF configuration - # 2. It has a single subfolder that matches the nodename - # of this system, - - # Check for previous config key - if os.path.exists(configPath): - f = open(configPath, 'r') - for line in f.readlines(): - key, val = line.strip().split('=') - if key.strip() == "target_key": - targetDirKey = val.strip() - break - - # Examine anything else in the directory - self._targetSelectionError = None - dirList = [d for d in os.listdir(basePath) if - d != '.rsync-config'] - os.chdir(basePath) - if len(dirList) > 0: - msg = _("\'%s\'\n is not an empty device.\n\n" - "Please select an empty device.") \ - % (newTargetDir) - # No config key or > 1 directory: - # User specified a non empty directory. - if targetDirKey == None or len(dirList) > 1: - self._rsync_config_error(msg) - return False - # Make sure the single item is not a file or symlink. - elif os.path.islink(dirList[0]) or \ - os.path.isfile(dirList[0]): - self._rsync_config_error(msg) - return False - else: - # Has 1 other item and a config key. Other - # item must be a directory and must match the - # system nodename and SMF's key value respectively - # respectively - if dirList[0] != nodeName and \ - targetDirKey != self._smfTargetKey: - msg = _("\'%s\'\n" - "is a Time Slider external backup device " - "that is already in use by another system. " - "Backup devices may not be shared between " - "systems." - "\n\nPlease use a different device.") \ - % (newTargetDir) - self._rsync_config_error(msg) - return False - else: - if dirList[0] == nodeName and \ - targetDirKey != self._smfTargetKey: - # Looks like a device that we previously used, - # but discontinued using in favour of some other - # device. - msg = _("\'%s\' appears to be a a device " - "previously configured for use by this " - "system.\n\nDo you want resume use of " - "this device for backups?") \ - % (newTargetDir) - - topLevel = self._xml.get_widget("toplevel") - dialog = gtk.MessageDialog(topLevel, - 0, - gtk.MESSAGE_QUESTION, - gtk.BUTTONS_YES_NO, - _("Time Slider")) - dialog.set_default_response(gtk.RESPONSE_NO) - dialog.set_transient_for(topLevel) - dialog.set_markup(msg) - dialog.set_icon_name("time-slider-setup") - - response = dialog.run() - dialog.hide() - if response == gtk.RESPONSE_NO: - return False - else: - # Appears to be our own pre-configured directory. - self._newRsyncTargetSelected = False - - # Compare device ID against selected ZFS filesystems - # and their enclosing Zpools. The aim is to avoid - # a vicous circle caused by backing up snapshots onto - # the same pool the snapshots originate from - targetDev = os.stat(newTargetDir).st_dev - try: - fs = self._fsDevices[targetDev] - - # See if the filesystem itself is selected - # and/or any other fileystem on the pool is - # selected. - fsEnabled = self._snapStateDic[fs.name] - if fsEnabled == True: - # Definitely can't use this since it's a - # snapshotted filesystem. - msg = _("\'%s\'\n" - "belongs to the ZFS filesystem \'%s\' " - "which is already selected for " - "regular ZFS snaphots." - "\n\nPlease select a drive " - "not already in use by " - "Time Slider") \ - % (newTargetDir, fs.name) - self._rsync_config_error(msg) - return False - else: - # See if there is anything else on the pool being - # snapshotted - poolName = fs.name.split("/", 1)[0] - for name,mount in self._datasets.list_filesystems(): - if name.find(poolName) == 0: - try: - otherEnabled = self._snapStateDic[name] - radioBtn = self._xml.get_widget("defaultfsradio") - snapAll = radioBtn.get_active() - if snapAll or otherEnabled: - msg = _("\'%s\'\n" - "belongs to the ZFS pool \'%s\' " - "which is already being used " - "to store ZFS snaphots." - "\n\nPlease select a drive " - "not already in use by " - "Time Slider") \ - % (newTargetDir, poolName) - self._rsync_config_error(msg) - return False - except KeyError: - pass - except KeyError: - # No match found - good. - pass - - - # Figure out if there's a reasonable amount of free space to - # store backups. This is a vague guess at best. - allPools = zfs.list_zpools() - snapPools = [] - # FIXME - this is for custom selection. There is a short - # circuit case for default (All) configuration. Don't forget - # to implement this short circuit. - for poolName in allPools: - try: - snapPools.index(poolName) - except ValueError: - pool = zfs.ZPool(poolName) - # FIXME - we should include volumes here but they - # can only be set from the command line, not via - # the GUI, so not crucial. - for fsName,mount in pool.list_filesystems(): - # Don't try to catch exception. The filesystems - # are already populated in self._snapStateDic - enabled = self._snapStateDic[fsName] - if enabled == True: - snapPools.append(poolName) - break - - sumPoolSize = 0 - for poolName in snapPools: - pool = zfs.ZPool(poolName) - # Rough calcualation, but precise enough for - # estimation purposes - sumPoolSize += pool.get_used_size() - sumPoolSize += pool.get_available_size() - - - # Compare with available space on rsync target dir - targetAvail = util.get_available_size(targetMountPoint) - targetUsed = util.get_used_size(targetMountPoint) - targetSum = targetAvail + targetUsed - - # Recommended Minimum: - # At least double the combined size of all pools with - # fileystems selected for backup. Variables include, - # frequency of data changes, how much efficiency rsync - # sacrifices compared to ZFS' block level diff tracking, - # whether compression and/or deduplication are enabled - # on the source pool/fileystem. - # We don't try to make calculations based on individual - # filesystem selection as there are too many unpredictable - # variables to make an estimation of any practical use. - # Let the user figure that out for themselves. - - # The most consistent measurement is to use the sum of - # available and used size on the target fileystem. We - # assume based on previous checks that the target device - # is only being used for rsync backups and therefore the - # used value consists of existing backups and is. Available - # space can be reduced for various reasons including the used - # value increasing or for nfs mounted zfs fileystems, other - # zfs filesystems on the containing pool using up more space. - - - targetPoolRatio = targetSum/float(sumPoolSize) - if (targetPoolRatio < RSYNCTARGETRATIO): - response = self._rsync_size_warning(snapPools, - sumPoolSize, - targetMountPoint, - targetSum) - if response == False: - return False - - self._newRsyncTargetDir = targetMountPoint - return True - - def _on_ok_clicked(self, widget): - # Make sure the dictionaries are empty. - self._fsIntentDic = {} - self._snapStateDic = {} - self._rsyncStateDic = {} - enabled = self._xml.get_widget("enablebutton").get_active() - self._rsyncEnabled = self._xml.get_widget("rsyncbutton").get_active() - if enabled == False: - if self._rsyncEnabled == False and \ - self._initialRsyncState == True: - self._rsyncSMF.disable_service() - if self._initialEnabledState == True: - self._sliderSMF.disable_service() - # Ignore other changes to the snapshot/rsync configuration - # of filesystems. Just broadcast the change and exit. - self._configNotify = True - self.broadcast_changes() - gtk.main_quit() - else: - model = self._fsTreeView.get_model() - snapalldata = self._xml.get_widget("defaultfsradio").get_active() - - if snapalldata == True: - model.foreach(self._set_fs_selection_state, True) - if self._rsyncEnabled == True: - model.foreach(self._set_rsync_selection_state, True) - else: - model.foreach(self._get_fs_selection_state) - model.foreach(self._get_rsync_selection_state) - for fsname in self._snapStateDic: - self._refine_filesys_actions(fsname, - self._snapStateDic, - self._fsIntentDic) - if self._rsyncEnabled == True: - self._refine_filesys_actions(fsname, - self._rsyncStateDic, - self._rsyncIntentDic) - if self._rsyncEnabled and \ - not self._check_rsync_config(): - return - - self._pulseDialog.show() - self._enabler = EnableService(self) - self._enabler.start() - glib.timeout_add(100, - self._monitor_setup, - self._xml.get_widget("pulsebar")) - - def _on_enablebutton_toggled(self, widget): - expander = self._xml.get_widget("expander") - enabled = widget.get_active() - self._xml.get_widget("filesysframe").set_sensitive(enabled) - expander.set_sensitive(enabled) - if (enabled == False): - expander.set_expanded(False) - - def _on_rsyncbutton_toggled(self, widget): - self._rsyncEnabled = widget.get_active() - if self._rsyncEnabled == True: - self._fsTreeView.insert_column(self._rsyncRadioColumn, 1) - self._rsyncCombo.set_sensitive(True) - else: - self._fsTreeView.remove_column(self._rsyncRadioColumn) - self._rsyncCombo.set_sensitive(False) - - def _on_defaultfsradio_toggled(self, widget): - if widget.get_active() == True: - self._xml.get_widget("fstreeview").set_sensitive(False) - - def _on_selectfsradio_toggled(self, widget): - if widget.get_active() == True: - self._xml.get_widget("fstreeview").set_sensitive(True) - - def _advancedbox_unmap(self, widget): - # Auto shrink the window by subtracting the frame's height - # requistion from the window's height requisition - myrequest = widget.size_request() - toplevel = self._xml.get_widget("toplevel") - toprequest = toplevel.size_request() - toplevel.resize(toprequest[0], toprequest[1] - myrequest[1]) - - def _get_fs_selection_state(self, model, path, iter): - fsname = self._fsListStore.get_value(iter, 3) - enabled = self._fsListStore.get_value(iter, 0) - self._snapStateDic[fsname] = enabled - - def _get_rsync_selection_state(self, model, path, iter): - fsname = self._fsListStore.get_value(iter, 3) - enabled = self._fsListStore.get_value(iter, 1) - self._rsyncStateDic[fsname] = enabled - - def _set_fs_selection_state(self, model, path, iter, selected): - fsname = self._fsListStore.get_value(iter, 3) - self._snapStateDic[fsname] = selected - - def _set_rsync_selection_state(self, model, path, iter, selected): - fsname = self._fsListStore.get_value(iter, 3) - self._rsyncStateDic[fsname] = selected - - def _refine_filesys_actions(self, fsname, inputdic, actions): - selected = inputdic[fsname] - try: - fstag = actions[fsname] - # Found so we can skip over. - except KeyError: - # Need to check parent value to see if - # we should set explicitly or just inherit. - path = fsname.rsplit("/", 1) - parentName = path[0] - if parentName == fsname: - # Means this filesystem is the root of the pool - # so we need to set it explicitly. - actions[fsname] = \ - FilesystemIntention(fsname, selected, False) - else: - parentIntent = None - inherit = False - # Check if parent is already set and if so whether to - # inherit or override with a locally set property value. - try: - # Parent has already been registered - parentIntent = actions[parentName] - except: - # Parent not yet set, so do that recursively to figure - # out if we need to inherit or set a local property on - # this child filesystem. - self._refine_filesys_actions(parentName, - inputdic, - actions) - parentIntent = actions[parentName] - if parentIntent.selected == selected: - inherit = True - actions[fsname] = \ - FilesystemIntention(fsname, selected, inherit) - - def _validate_rsync_target(self, path): - """ - Tests path to see if it is the pre-configured - rsync backup device path. - Returns True on success, otherwise False - """ - # FIXME - this is duplicate in applet.py and rsync-backup.py - # It should be moved into a shared module - if not os.path.exists(path): - return False - testDir = os.path.join(path, - rsyncsmf.RSYNCDIRPREFIX, - self._nodeName) - testKeyFile = os.path.join(path, - rsyncsmf.RSYNCDIRPREFIX, - rsyncsmf.RSYNCCONFIGFILE) - if os.path.exists(testDir) and \ - os.path.exists(testKeyFile): - testKeyVal = None - f = open(testKeyFile, 'r') - for line in f.readlines(): - key, val = line.strip().split('=') - if key.strip() == "target_key": - targetKey = val.strip() - break - f.close() - if targetKey == self._smfTargetKey: - return True - return False - - - def commit_filesystem_selection(self): - """ - Commits the intended filesystem selection actions based on the - user's UI configuration to disk. Compares with initial startup - configuration and applies the minimum set of necessary changes. - """ - for fsname,fsmountpoint in self._datasets.list_filesystems(): - fs = zfs.Filesystem(fsname, fsmountpoint) - try: - initialIntent = self._initialFsIntentDic[fsname] - intent = self._fsIntentDic[fsname] - if intent == initialIntent: - continue - fs.set_auto_snap(intent.selected, intent.inherited) - - except KeyError: - pass - - def commit_rsync_selection(self): - """ - Commits the intended filesystem selection actions based on the - user's UI configuration to disk. Compares with initial startup - configuration and applies the minimum set of necessary changes. - """ - for fsname,fsmountpoint in self._datasets.list_filesystems(): - fs = zfs.Filesystem(fsname, fsmountpoint) - try: - initialIntent = self._initialRsyncIntentDic[fsname] - intent = self._rsyncIntentDic[fsname] - if intent == initialIntent: - continue - if intent.inherited == True and \ - initialIntent.inherited == False: - fs.unset_user_property(rsyncsmf.RSYNCFSTAG) - else: - if intent.selected == True: - value = "true" - else: - value = "false" - fs.set_user_property(rsyncsmf.RSYNCFSTAG, - value) - except KeyError: - pass - - def setup_rsync_config(self): - if self._rsyncEnabled == True: - if self._newRsyncTargetSelected == True: - sys,nodeName,rel,ver,arch = os.uname() - basePath = os.path.join(self._newRsyncTargetDir, - rsyncsmf.RSYNCDIRPREFIX,) - nodePath = os.path.join(basePath, - nodeName) - configPath = os.path.join(basePath, - rsyncsmf.RSYNCCONFIGFILE) - newKey = generate_random_key() - try: - origmask = os.umask(0222) - if not os.path.exists(nodePath): - os.makedirs(nodePath, 0755) - f = open(configPath, 'w') - f.write("target_key=%s\n" % (newKey)) - f.close() - os.umask(origmask) - except OSError as e: - self._pulseDialog.hide() - sys.stderr.write("Error configuring external " \ - "backup device:\n" \ - "%s\n\nReason:\n %s") \ - % (self._newRsyncTargetDir, str(e)) - sys.exit(-1) - self._rsyncSMF.set_target_dir(self._newRsyncTargetDir) - self._rsyncSMF.set_target_key(newKey) - # Applet monitors rsyncTargetDir so make sure to notify it. - self._configNotify = True - return - - def setup_services(self): - # Take care of the rsync plugin service first since time-slider - # will query it. - # Changes to rsync or time-slider SMF service State should be - # broadcast to let notification applet refresh. - if self._rsyncEnabled == True and \ - self._initialRsyncState == False: - self._rsyncSMF.enable_service() - self._configNotify = True - elif self._rsyncEnabled == False and \ - self._initialRsyncState == True: - self._rsyncSMF.disable_service() - self._configNotify = True - customSelection = self._xml.get_widget("selectfsradio").get_active() - if customSelection != self._initialCustomSelection: - self._sliderSMF.set_custom_selection(customSelection) - if self._initialEnabledState == False: - enable_default_schedules() - self._sliderSMF.enable_service() - self._configNotify = True - - def set_cleanup_level(self): - """ - Wrapper function to set the warning level cleanup threshold - value as a percentage of pool capacity. - """ - level = self._xml.get_widget("capspinbutton").get_value_as_int() - if level != self._initialCleanupLevel: - self._sliderSMF.set_cleanup_level("warning", level) - - def broadcast_changes(self): - """ - Blunt instrument to notify D-Bus listeners such as notification - applet to rescan service configuration - """ - if self._configNotify == False: - return - self._dbus.config_changed() - - def _on_deletesnapshots_clicked(self, widget): - cmdpath = os.path.join(os.path.dirname(self._execPath), \ - "../lib/time-slider-delete") - p = subprocess.Popen(cmdpath, close_fds=True) - - -class EnableService(threading.Thread): - - def __init__(self, setupManager): - threading.Thread.__init__(self) - self._setupManager = setupManager - - def run(self): - try: - # Set the service state last so that the ZFS filesystems - # are correctly tagged before the snapshot scripts check them - self._setupManager.commit_filesystem_selection() - self._setupManager.commit_rsync_selection() - self._setupManager.set_cleanup_level() - self._setupManager.setup_rsync_config() - self._setupManager.setup_services() - self._setupManager.broadcast_changes() - except RuntimeError, message: - sys.stderr.write(str(message)) - -def generate_random_key(length=32): - """ - Returns a 'length' byte character composed of random letters and - unsigned single digit integers. Used to create a random - signature key to identify pre-configured backup directories - for the rsync plugin - """ - from string import letters, digits - from random import choice - return ''.join([choice(letters + digits) \ - for i in range(length)]) - -def main(argv): - rbacp = RBACprofile() - # The setup GUI needs to be run as root in order to ensure - # that the rsync backup target directory is accessible by - # root and to perform validation checks on it. - # This GUI can be launched with an euid of root in one of - # the following 3 ways; - # 0. Run by the superuser (root) - # 1. Run via gksu to allow a non priviliged user to authenticate - # as the superuser (root) - - if os.geteuid() == 0: - manager = SetupManager(argv) - gtk.gdk.threads_enter() - gtk.main() - gtk.gdk.threads_leave() - elif os.path.exists(argv) and os.path.exists("/usr/bin/gksu"): - # Run via gksu, which will prompt for the root password - os.unsetenv("DBUS_SESSION_BUS_ADDRESS") - os.execl("/usr/bin/gksu", "gksu", argv) - # Shouldn't reach this point - sys.exit(1) - else: - dialog = gtk.MessageDialog(None, - 0, - gtk.MESSAGE_ERROR, - gtk.BUTTONS_CLOSE, - _("Insufficient Priviliges")) - dialog.format_secondary_text(_("The snapshot manager service requires " - "administrative privileges to run. " - "You have not been assigned the necessary" - "administrative priviliges." - "\n\nConsult your system administrator ")) - dialog.set_icon_name("time-slider-setup") - dialog.run() - sys.exit(1) -