5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
29 from autosnapsmf import enable_default_schedules, disable_default_schedules
31 from os.path import abspath, dirname, join, pardir
32 sys.path.insert(0, join(dirname(__file__), pardir, "plugin"))
34 sys.path.insert(0, join(dirname(__file__), pardir, "plugin", "rsync"))
45 gtk.gdk.threads_init()
55 import dbus.mainloop.glib
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.
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__),
70 LOCALE_PATH = os.path.join('/usr', 'share', 'locale')
71 RESOURCE_PATH = os.path.join(SHARED_FILES, 'res')
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'
78 # set up the glade gettext system and locales
79 gtk.glade.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH)
80 gtk.glade.textdomain(GETTEXT_DOMAIN)
83 from timeslidersmf import TimeSliderSMF
84 from rbac import RBACprofile
87 class FilesystemIntention:
89 def __init__(self, name, selected, inherited):
91 self.selected = selected
92 self.inherited = inherited
95 return_string = "Filesystem name: " + self.name + \
96 "\n\tSelected: " + str(self.selected) + \
97 "\n\tInherited: " + str(self.inherited)
100 def __eq__(self, other):
101 if self.name != other.name:
103 if self.inherited and other.inherited:
105 elif not self.inherited and other.inherited:
107 if (self.selected == other.selected) and \
108 (self.inherited == other.inherited):
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__)))
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()
125 # Register a bus name with the system dbus daemon
126 systemBus = dbus.SystemBus()
127 busName = dbus.service.BusName("org.opensolaris.TimeSlider.config",
129 self._dbus = dbussvc.Config(systemBus,
130 '/org/opensolaris/TimeSlider/config')
131 # Used later to trigger a D-Bus notification of select configuration
133 self._configNotify = False
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 = {}
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
161 topLevel = self._xml.get_widget("toplevel")
162 self._pulseDialog = self._xml.get_widget("pulsedialog")
163 self._pulseDialog.set_transient_for(topLevel)
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)
170 self._fsListStore = gtk.ListStore(bool,
174 gobject.TYPE_PYOBJECT)
175 filesystems = self._datasets.list_filesystems()
176 for fsname,fsmountpoint in filesystems:
177 if (fsmountpoint == "legacy"):
178 mountpoint = _("Legacy")
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":
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
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)
207 self._fsTreeView = self._xml.get_widget("fstreeview")
208 self._fsTreeView.set_sensitive(False)
209 self._fsTreeView.set_size_request(10, 200)
211 self._fsTreeView.set_model(self._fsListStore)
213 cell0 = gtk.CellRendererToggle()
214 cell1 = gtk.CellRendererToggle()
215 cell2 = gtk.CellRendererText()
216 cell3 = gtk.CellRendererText()
218 radioColumn = gtk.TreeViewColumn(_("Select"),
220 self._fsTreeView.insert_column(radioColumn, 0)
222 self._rsyncRadioColumn = gtk.TreeViewColumn(_("Replicate"),
224 nameColumn = gtk.TreeViewColumn(_("Mount Point"),
226 self._fsTreeView.insert_column(nameColumn, 2)
227 mountPointColumn = gtk.TreeViewColumn(_("File System Name"),
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)
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()
248 # 0 Themed icon list (python list)
251 # 3 Is gio.Mount device
252 # 4 Is separator (for comboBox separator rendering)
253 self._rsyncStore = gtk.ListStore(gobject.TYPE_PYOBJECT,
256 gobject.TYPE_BOOLEAN,
257 gobject.TYPE_BOOLEAN)
258 self._rsyncCombo = self._xml.get_widget("rsyncdevcombo")
259 mounts = self._vm.get_mounts()
261 self._mount_added(self._vm, mount)
264 self._rsyncStore.append((None, None, None, None, True))
267 if len(self._newRsyncTargetDir) == 0:
268 self._rsyncStore.append((['folder'],
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)
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)
301 if state != "disabled":
302 self._rsyncEnabled = True
303 self._xml.get_widget("rsyncbutton").set_active(True)
304 self._initialRsyncState = True
306 self._rsyncEnabled = False
307 self._rsyncCombo.set_sensitive(False)
308 self._initialRsyncState = False
310 # Initialise SMF service instance state.
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"),
319 _("Snapshot manager service error"))
320 dialog.format_secondary_text(_("The snapshot manager service does "
321 "not appear to be installed on this "
323 "\n\nSee the svcs(1) man page for more "
325 "\n\nDetails:\n%s")%(message))
326 dialog.set_icon_name("time-slider-setup")
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"),
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")
351 elif self._sliderSMF.svcstate == "maintenance":
352 self._xml.get_widget("toplevel").set_sensitive(False)
353 dialog = gtk.MessageDialog(self._xml.get_widget("toplevel"),
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 "
363 dialog.set_icon_name("time-slider-setup")
367 # FIXME: Check transitional states
368 self._xml.get_widget("enablebutton").set_active(True)
369 self._initialEnabledState = True
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)
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")
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
396 spinButton.set_value(warnLevel)
398 spinButton.set_value(70)
400 def _icon_cell_render(self, celllayout, cell, model, iter):
401 iconList = self._rsyncStore.get_value(iter, 0)
403 gicon = gio.ThemedIcon(iconList)
404 cell.set_property("gicon", gicon)
406 root = self._rsyncStore.get_value(iter, 2)
408 cell.set_property("gicon", None)
410 def _row_separator(self, model, iter):
411 return model.get_value(iter, 4)
413 def _mount_added(self, volume_monitor, mount):
414 icon = mount.get_icon()
415 iconList = icon.get_names()
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()
423 volName = mount.get_name()
425 volName = os.path.split(path)[1]
427 volName = volume.get_name()
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
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))
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
440 if self._initialRsyncTargetDir == self._newRsyncTargetDir:
441 if self._validate_rsync_target(path) == True:
442 self._rsyncCombo.set_active(0)
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()
450 # Search gio.Mount devices
451 while iter != None and \
452 self._rsyncStore.get_value(iter, 3) == True:
454 compPath = self._rsyncStore.get_value(iter, 2)
459 iter = self._rsyncStore.iter_next(iter)
460 if mountIter != None:
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
475 name = os.path.split(path)[1]
478 else: # Indicates path is unset: ''
480 iter = self._rsyncStore.insert_before(iter,
486 iter = self._rsyncStore.insert_before(iter,
492 self._rsyncCombo.set_active_iter(iter)
494 def _monitor_setup(self, pulseBar):
495 if self._enabler.isAlive() == True:
501 def _row_toggled(self, renderer, path):
502 model = self._fsTreeView.get_model()
503 iter = model.get_iter(path)
504 state = renderer.get_active()
506 self._fsListStore.set_value(iter, 0, True)
508 self._fsListStore.set_value(iter, 0, False)
509 self._fsListStore.set_value(iter, 1, False)
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)
518 self._fsListStore.set_value(iter, 1, True)
520 self._fsListStore.set_value(iter, 1, False)
522 def _rsync_config_error(self, msg):
523 topLevel = self._xml.get_widget("toplevel")
524 dialog = gtk.MessageDialog(topLevel,
528 _("Unsuitable Backup Location"))
529 dialog.format_secondary_text(msg)
530 dialog.set_icon_name("time-slider-setup")
535 def _rsync_dev_selected(self, path):
536 iter = self._rsyncStore.get_iter_first()
538 # Break out when we hit a non gio.Mount device
539 if self._rsyncStore.get_value(iter, 3) == False:
541 compPath = self._rsyncStore.get_value(iter, 2)
543 self._rsyncCombo.set_active_iter(iter)
544 self._newRsyncTargetDir = path
547 iter = self._rsyncStore.iter_next(iter)
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)
558 name = os.path.split(path)[1]
561 else: # Indicates path is unset: ''
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,
574 iter = self._rsyncStore.insert_before(iter,
581 self._rsyncStore.set(iter,
584 self._rsyncCombo.set_active_iter(iter)
585 self._newRsyncTargetDir = path
587 def _rsync_combo_changed(self, combobox):
588 newIter = combobox.get_active_iter()
590 root = self._rsyncStore.get_value(newIter, 2)
592 self._newRsyncTargetDir = root
594 msg = _("Select A Back Up Device")
596 gtk.FileChooserDialog(
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),
603 self._rsyncCombo.set_sensitive(False)
604 response = fileDialog.run()
606 if response == gtk.RESPONSE_OK:
607 gFile = fileDialog.get_file()
608 self._rsync_dev_selected(gFile.get_path())
610 self._rsync_dev_selected(self._newRsyncTargetDir)
611 self._rsyncCombo.set_sensitive(True)
613 def _rsync_size_warning(self, zpools, zpoolSize,
614 rsyncTarget, targetSize):
615 # Using decimal "GB" instead of binary "GiB"
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)
627 sizeStr = "%.1f MB" % round(suggestedSize / float(MB), 1)
630 targetStr = "%.1f TB" % round(targetSize / float(TB), 1)
631 elif targetSize > GB:
632 targetStr = "%.1f GB" % round(targetSize / float(GB), 1)
634 targetStr = "%.1f MB" % round(targetSize / float(MB), 1)
637 msg = _("Time Slider suggests a device with a capacity of at "
639 "The device: \'<b>%s</b>\'\nonly has <b>%s</b>\n"
640 "Do you want to use it anyway?") \
641 % (sizeStr, rsyncTarget, targetStr)
643 topLevel = self._xml.get_widget("toplevel")
644 dialog = gtk.MessageDialog(topLevel,
646 gtk.MESSAGE_QUESTION,
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")
654 response = dialog.run()
656 if response == gtk.RESPONSE_YES:
661 def _check_rsync_config(self):
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.
669 def _get_mount_point(path):
670 if os.path.ismount(path):
673 return _get_mount_point(abspath(join(path, pardir)))
675 if self._rsyncEnabled != True:
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)
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):
690 # Perform the required validation checks on the
692 newTargetDir = self._newRsyncTargetDir
694 # We require the whole device. So find the enclosing
695 # mount point and inspect from there.
696 targetMountPoint = abspath(_get_mount_point(newTargetDir))
698 # Check that it's writable.
700 testFile = os.path.join(targetMountPoint, ".ts-test")
702 f = open(testFile, 'w')
703 except (OSError, IOError):
705 "is not writable. The backup device must "
706 "be writable by the system administrator."
707 "\n\nPlease use a different device.") \
709 self._rsync_config_error(msg)
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)
718 os.link(testFile, ".ts-test-link")
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.") \
727 self._rsync_config_error(msg)
731 os.unlink(".ts-test-link")
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,
740 configPath = os.path.join(basePath,
741 rsyncsmf.RSYNCCONFIGFILE)
742 self._newRsyncTargetSelected = True
745 contents = os.listdir(targetMountPoint)
746 os.chdir(targetMountPoint)
748 # The only other exception to an empty directory is
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.") \
758 self._rsync_config_error(msg)
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
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()
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']
784 msg = _("\'%s\'\n is not an empty device.\n\n"
785 "Please select an empty device.") \
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)
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)
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
802 if dirList[0] != nodeName and \
803 targetDirKey != self._smfTargetKey:
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 "
809 "\n\nPlease use a different device.") \
811 self._rsync_config_error(msg)
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
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?") \
825 topLevel = self._xml.get_widget("toplevel")
826 dialog = gtk.MessageDialog(topLevel,
828 gtk.MESSAGE_QUESTION,
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")
836 response = dialog.run()
838 if response == gtk.RESPONSE_NO:
841 # Appears to be our own pre-configured directory.
842 self._newRsyncTargetSelected = False
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
850 fs = self._fsDevices[targetDev]
852 # See if the filesystem itself is selected
853 # and/or any other fileystem on the pool is
855 fsEnabled = self._snapStateDic[fs.name]
856 if fsEnabled == True:
857 # Definitely can't use this since it's a
858 # snapshotted filesystem.
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 "
866 % (newTargetDir, fs.name)
867 self._rsync_config_error(msg)
870 # See if there is anything else on the pool being
872 poolName = fs.name.split("/", 1)[0]
873 for name,mount in self._datasets.list_filesystems():
874 if name.find(poolName) == 0:
876 otherEnabled = self._snapStateDic[name]
877 radioBtn = self._xml.get_widget("defaultfsradio")
878 snapAll = radioBtn.get_active()
879 if snapAll or otherEnabled:
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 "
887 % (newTargetDir, poolName)
888 self._rsync_config_error(msg)
893 # No match found - good.
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()
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:
906 snapPools.index(poolName)
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]
917 snapPools.append(poolName)
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()
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
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.
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.
956 targetPoolRatio = targetSum/float(sumPoolSize)
957 if (targetPoolRatio < RSYNCTARGETRATIO):
958 response = self._rsync_size_warning(snapPools,
962 if response == False:
965 self._newRsyncTargetDir = targetMountPoint
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()
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()
987 model = self._fsTreeView.get_model()
988 snapalldata = self._xml.get_widget("defaultfsradio").get_active()
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)
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,
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():
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"))
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)
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)
1030 self._fsTreeView.remove_column(self._rsyncRadioColumn)
1031 self._rsyncCombo.set_sensitive(False)
1033 def _on_defaultfsradio_toggled(self, widget):
1034 if widget.get_active() == True:
1035 self._xml.get_widget("fstreeview").set_sensitive(False)
1037 def _on_selectfsradio_toggled(self, widget):
1038 if widget.get_active() == True:
1039 self._xml.get_widget("fstreeview").set_sensitive(True)
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])
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
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
1059 def _set_fs_selection_state(self, model, path, iter, selected):
1060 fsname = self._fsListStore.get_value(iter, 3)
1061 self._snapStateDic[fsname] = selected
1063 def _set_rsync_selection_state(self, model, path, iter, selected):
1064 fsname = self._fsListStore.get_value(iter, 3)
1065 self._rsyncStateDic[fsname] = selected
1067 def _refine_filesys_actions(self, fsname, inputdic, actions):
1068 selected = inputdic[fsname]
1070 fstag = actions[fsname]
1071 # Found so we can skip over.
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.
1081 FilesystemIntention(fsname, selected, False)
1085 # Check if parent is already set and if so whether to
1086 # inherit or override with a locally set property value.
1088 # Parent has already been registered
1089 parentIntent = actions[parentName]
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,
1097 parentIntent = actions[parentName]
1098 if parentIntent.selected == selected:
1101 FilesystemIntention(fsname, selected, inherit)
1103 def _validate_rsync_target(self, path):
1105 Tests path to see if it is the pre-configured
1106 rsync backup device path.
1107 Returns True on success, otherwise False
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):
1113 testDir = os.path.join(path,
1114 rsyncsmf.RSYNCDIRPREFIX,
1116 testKeyFile = os.path.join(path,
1117 rsyncsmf.RSYNCDIRPREFIX,
1118 rsyncsmf.RSYNCCONFIGFILE)
1119 if os.path.exists(testDir) and \
1120 os.path.exists(testKeyFile):
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()
1129 if targetKey == self._smfTargetKey:
1134 def commit_filesystem_selection(self):
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.
1140 for fsname,fsmountpoint in self._datasets.list_filesystems():
1141 fs = zfs.Filesystem(fsname, fsmountpoint)
1143 initialIntent = self._initialFsIntentDic[fsname]
1144 intent = self._fsIntentDic[fsname]
1145 if intent == initialIntent:
1147 fs.set_auto_snap(intent.selected, intent.inherited)
1152 def commit_rsync_selection(self):
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.
1158 for fsname,fsmountpoint in self._datasets.list_filesystems():
1159 fs = zfs.Filesystem(fsname, fsmountpoint)
1161 initialIntent = self._initialRsyncIntentDic[fsname]
1162 intent = self._rsyncIntentDic[fsname]
1163 if intent == initialIntent:
1165 if intent.inherited == True and \
1166 initialIntent.inherited == False:
1167 fs.unset_user_property(rsyncsmf.RSYNCFSTAG)
1169 if intent.selected == True:
1173 fs.set_user_property(rsyncsmf.RSYNCFSTAG,
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,
1186 configPath = os.path.join(basePath,
1187 rsyncsmf.RSYNCCONFIGFILE)
1188 newKey = generate_random_key()
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))
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))
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
1210 def setup_services(self):
1211 # Take care of the rsync plugin service first since time-slider
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
1231 def set_cleanup_level(self):
1233 Wrapper function to set the warning level cleanup threshold
1234 value as a percentage of pool capacity.
1236 level = self._xml.get_widget("capspinbutton").get_value_as_int()
1237 if level != self._initialCleanupLevel:
1238 self._sliderSMF.set_cleanup_level("warning", level)
1240 def broadcast_changes(self):
1242 Blunt instrument to notify D-Bus listeners such as notification
1243 applet to rescan service configuration
1245 if self._configNotify == False:
1247 self._dbus.config_changed()
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)
1255 class EnableService(threading.Thread):
1257 def __init__(self, setupManager):
1258 threading.Thread.__init__(self)
1259 self._setupManager = setupManager
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))
1274 def generate_random_key(length=32):
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
1281 from string import letters, digits
1282 from random import choice
1283 return ''.join([choice(letters + digits) \
1284 for i in range(length)])
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)
1297 if os.geteuid() == 0:
1298 manager = SetupManager(argv)
1299 gtk.gdk.threads_enter()
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
1309 dialog = gtk.MessageDialog(None,
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")