Beispiel #1
0
 def rescan(self):
     cloned = self.datasets.list_cloned_snapshots()
     self.snapshots = []
     snaplist = self.datasets.list_snapshots()
     for snapname, snaptime in snaplist:
         # Filter out snapshots that are the root
         # of cloned filesystems or volumes
         try:
             cloned.index(snapname)
         except ValueError:
             snapshot = zfs.Snapshot(snapname, snaptime)
             self.snapshots.append(snapshot)
Beispiel #2
0
def main(argv):

    # Check appropriate environment variables habe been supplied
    # by time-slider
    #
    # The label used for the snapshot set just taken, ie. the
    # component proceeding the "@" in the snapshot name
    snaplabel = os.getenv("AUTOSNAP_LABEL")
    # The SMF fmri of the auto-snapshot instance corresponding to
    # the snapshot set just taken.
    snapfmri = os.getenv("AUTOSNAP_FMRI")
    # The SMF fmri of the time-slider plugin instance associated with
    # this command.
    pluginfmri = os.getenv("PLUGIN_FMRI")

    if pluginfmri == None:
        sys.stderr.write("No time-slider plugin SMF instance FMRI defined. " \
                         "This plugin does not support command line "
                         "execution. Exiting\n")
        sys.exit(-1)
    syslog.openlog(pluginfmri, 0, syslog.LOG_DAEMON)

    cmd = [smf.SVCPROPCMD, "-p", verboseprop, pluginfmri]
    outdata,errdata = util.run_command(cmd)
    if outdata.rstrip() == "true":
        verbose = True
    else:
        verbose = False

    if snaplabel == None:
        log_error(syslog.LOG_ERR,
                  "No snapshot label defined. Exiting")
        sys.exit(-1)
    if snapfmri == None:
        log_error(syslog.LOG_ERR,
                  "No auto-snapshot SMF instance FMRI defined. Exiting")
        sys.exit(-1)

    schedule = snapfmri.rsplit(':', 1)[1]
    plugininstance = pluginfmri.rsplit(':', 1)[1]

    # The user property/tag used when tagging and holding zfs datasets
    propname = "%s:%s" % (propbasename, plugininstance)

    # Identifying snapshots is a two stage process.
    #
    # First: identify all snapshots matching the AUTOSNAP_LABEL
    # value passed in by the time-slider daemon.
    #
    # Second: we need to filter the results and ensure that the
    # filesystem/voluem corresponding to each snapshot is actually
    # tagged with the property (com.sun:auto-snapshot<:schedule>)
    #
    # This is necessary to avoid confusion whereby a snapshot might
    # have been sent|received from one zpool to another on the same
    # system. The received snapshot will show up in the first pass
    # results but is not actually part of the auto-snapshot set
    # created by time-slider. It also avoids incorrectly placing
    # zfs holds on the imported snapshots.

    datasets = zfs.Datasets()
    candidates = datasets.list_snapshots(snaplabel)
    originsets = datasets.list_auto_snapshot_sets(schedule)
    snappeddatasets = []
    snapnames = [name for [name,ctime] in candidates \
                 if name.split('@',1)[0] in originsets]


    # Place a hold on the the newly created snapshots so
    # they can be backed up without fear of being destroyed
    # before the backup gets a chance to complete.
    for snap in snapnames:
        snapshot = zfs.Snapshot(snap)
        holds = snapshot.holds()
        try:
            holds.index(propname)
        except ValueError:
            util.debug("Placing hold on %s" % (snap), verbose)
            snapshot.hold(propname)
        datasetname = snapshot.fsname
        # Insert datasetnames in alphabetically sorted order because
        # zfs receive falls over if it receives a child before the
        # parent if the "-F" option is not used.
        insort(snappeddatasets, datasetname)

    # Find out the receive command property value
    cmd = [smf.SVCPROPCMD, "-c", "-p", "receive/command", pluginfmri]
    outdata,errdata = util.run_command(cmd)
    # Strip out '\' characters inserted by svcprop
    recvcmd = outdata.strip().replace('\\', '').split()

    # Check to see if the receive command is accessible and executable
    try:
        statinfo = os.stat(recvcmd[0])
        other_x = (statinfo.st_mode & 01)
        if other_x == 0:
            log_error(syslog.LOG_ERR,
                      "Plugin: %s: Configured receive/command is not " \
                      "executable: %s" \
                      % (pluginfmri, outdata))
            maintenance(pluginfmri)
            sys.exit(-1)
    except OSError:
        log_error(syslog.LOG_ERR,
                  "Plugin: %s: Can not access the configured " \
                  "receive/command: %s" \
                  % (pluginfmri, outdata)) 
        maintenance(pluginfmri)   
        sys.exit(-1)

    for dataset in snappeddatasets:
        sendcmd = None
        prevsnapname = None
        ds = zfs.ReadableDataset(dataset)
        prevlabel = ds.get_user_property(propname)

        snapname = "%s@%s" % (ds.name, snaplabel)
        if (prevlabel == None or prevlabel == '-' or len(prevlabel) == 0):
            # No previous backup - send a full replication stream
            sendcmd = [zfs.ZFSCMD, "send", snapname]
            util.debug("No previous backup registered for %s" % ds.name, verbose)
        else:
            # A record of a previous backup exists.
            # Check that it exists to enable send of an incremental stream.
            prevsnapname = "%s@%s" % (ds.name, prevlabel)
            util.debug("Previously sent snapshot: %s" % prevsnapname, verbose)
            prevsnap = zfs.Snapshot(prevsnapname)
            if prevsnap.exists():
                sendcmd = [zfs.ZFSCMD, "send", "-i", prevsnapname, snapname]
            else:
                # This should not happen under normal operation since we
                # place a hold on the snapshot until it gets sent. So
                # getting here suggests that something else released the
                # hold on the snapshot, allowing it to get destroyed
                # prematurely.
                log_error(syslog.LOG_ERR,
                          "Previously sent snapshot no longer exists: %s" \
                          % prevsnapname)
                maintenance(pluginfmri)
                sys.exit(-1)
        
        
        # Invoke the send and receive commands via pfexec(1) since
        # we are not using the role's shell to take care of that
        # for us.
        sendcmd.insert(0, smf.PFCMD)
        recvcmd.insert(0, smf.PFCMD)

        try:
            sendP = subprocess.Popen(sendcmd,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE,
                                     close_fds=True)
            recvP = subprocess.Popen(recvcmd,
                                     stdin=sendP.stdout,
                                     stderr=subprocess.PIPE,
                                     close_fds=True)

            recvout,recverr = recvP.communicate()
            recverrno = recvP.wait()
            sendout,senderr = sendP.communicate()
            senderrno = sendP.wait()

            if senderrno != 0:
                raise RuntimeError, "Send command: %s failed with exit code" \
                                    "%d. Error message: \n%s" \
                                    % (str(sendcmd), senderrno, senderr)
            if recverrno != 0:
                raise RuntimeError, "Receive command %s failed with exit " \
                                    "code %d. Error message: \n%s" \
                                    % (str(recvcmd), recverrno, recverr)

            if prevsnapname != None:
                util.debug("Releasing hold on %s" % (prevsnapname), verbose)
                snapshot = zfs.Snapshot(prevsnapname)
                util.debug("Releasing hold on previous snapshot: %s" \
                      % (prevsnapname),
                      verbose)
                snapshot.release(propname)
        except Exception, message:
            log_error(syslog.LOG_ERR,
                      "Error during snapshot send/receive operation: %s" \
                      % (message))

            maintenance(pluginfmri)
            sys.exit(-1)            

        # Finally, after success, make a record of the latest backup
        # and release the old snapshot.
        ds.set_user_property(propname, snaplabel)
        util.debug("Sending of \"%s\"snapshot streams completed" \
              % (snaplabel),
              verbose)
Beispiel #3
0
def main(argv):
    # Check that appropriate environment variables have been
    # provided by time-sliderd
    #
    # The label used for the snapshot set just taken, ie. the
    # component proceeding the "@" in the snapshot name
    snaplabel = os.getenv("AUTOSNAP_LABEL")
    # The SMF fmri of the auto-snapshot instance corresponding to
    # the snapshot set just taken.
    snapfmri = os.getenv("AUTOSNAP_FMRI")
    # The SMF fmri of the time-slider plugin instance associated with
    # this command.
    pluginfmri = os.getenv("PLUGIN_FMRI")

    if pluginfmri == None:
        sys.stderr.write("No time-slider plugin SMF instance FMRI defined. " \
                         "This plugin does not support command line "
                         "execution. Exiting\n")
        sys.exit(-1)
    syslog.openlog(pluginfmri, 0, syslog.LOG_DAEMON)

    cmd = [smf.SVCPROPCMD, "-p", verboseprop, pluginfmri]
    outdata, errdata = util.run_command(cmd)
    if outdata.rstrip() == "true":
        verbose = True
    else:
        verbose = False

    if snaplabel == None:
        log_error(syslog.LOG_ERR, "No snapshot label provided. Exiting")
        sys.exit(-1)
    if snapfmri == None:
        log_error(syslog.LOG_ERR,
                  "No auto-snapshot SMF instance FMRI provided. Exiting")
        sys.exit(-1)

    schedule = snapfmri.rsplit(':', 1)[1]
    plugininstance = pluginfmri.rsplit(':', 1)[1]

    # The user property/tag used when tagging and holding zfs datasets
    propname = "%s:%s" % (propbasename, plugininstance)

    # Identifying snapshots is a 3 stage process.
    #
    # First: identify all snapshots matching the AUTOSNAP_LABEL
    # value passed in by the time-slider daemon.
    #
    # Second: Filter out snapshots of volumes, since rsync can only
    # back up filesystems.
    #
    # Third: we need to filter the results and ensure that the
    # filesystem corresponding to each snapshot is actually
    # tagged with the property (com.sun:auto-snapshot<:schedule>)
    #
    # This is necessary to avoid confusion whereby a snapshot might
    # have been sent|received from one zpool to another on the same
    # system. The received snapshot will show up in the first pass
    # results but is not actually part of the auto-snapshot set
    # created by time-slider. It also avoids incorrectly placing
    # zfs holds on the imported snapshots.

    datasets = zfs.Datasets()
    candidates = datasets.list_snapshots(snaplabel)
    autosnapsets = datasets.list_auto_snapshot_sets(schedule)
    autosnapfs = [name for [name,mount] in datasets.list_filesystems() \
                   if name in autosnapsets]
    snappeddatasets = []
    snapnames = [name for [name,ctime] in candidates \
                 if name.split('@',1)[0] in autosnapfs]

    # Mark the snapshots with a user property. Doing this instead of
    # placing a physical hold on the snapshot allows time-slider to
    # expire the snapshots naturally or destroy them if a zpool fills
    # up and triggers a remedial cleanup.
    # It also prevents the possiblity of leaving snapshots lying around
    # indefinitely on the system if the plugin SMF instance becomes
    # disabled or having to release a pile of held snapshots.
    # We set org.opensolaris:time-slider-plugin:<instance> to "pending",
    # indicate
    snapshots = []
    for snap in snapnames:
        snapshot = zfs.Snapshot(snap)
        fs = zfs.Filesystem(snapshot.fsname)
        if fs.get_user_property(rsyncsmf.RSYNCFSTAG) == "true":
            if fs.is_mounted() == True:
                snapshot.set_user_property(propname, "pending")
                util.debug("Marking %s as pending rsync" % (snap), verbose)
            else:
                util.debug("Ignoring snapshot of unmounted fileystem: %s" \
                           % (snap), verbose)
Beispiel #4
0
    def _run_cleanup(self, zpool, schedule, threshold):
        clonedsnaps = []
        snapshots = []
        try:
            clonedsnaps = self._datasets.list_cloned_snapshots()
        except RuntimeError as message:
            sys.stderr.write("Error (non-fatal) listing cloned snapshots" +
                             " while recovering pool capacity\n")
            sys.stderr.write("Error details:\n" + \
                             "--------BEGIN ERROR MESSAGE--------\n" + \
                             str(message) + \
                             "\n--------END ERROR MESSAGE--------\n")

        # Build a list of snapshots in the given schedule, that are not
        # cloned, and sort the result in reverse chronological order.
        try:
            snapshots = [s for s,t in \
                            zpool.list_snapshots("%s%s" \
                            % (self._prefix,schedule)) \
                            if not s in clonedsnaps]
            snapshots.reverse()
        except RuntimeError as message:
            sys.stderr.write("Error listing snapshots" +
                             " while recovering pool capacity\n")
            self.exitCode = smf.SMF_EXIT_ERR_FATAL
            # Propogate the error up to the thread's run() method.
            raise RuntimeError(message)

        while zpool.get_capacity() > threshold:
            if len(snapshots) == 0:
                syslog.syslog(syslog.LOG_NOTICE,
                              "No more %s snapshots left" \
                               % schedule)
                return
            """This is not an exact science. Deleteing a zero sized 
            snapshot can have unpredictable results. For example a
            pair of snapshots may share exclusive reference to a large
            amount of data (eg. a large core file). The usage of both
            snapshots will initially be seen to be 0 by zfs(1). Deleting
            one of the snapshots will make the data become unique to the
            single remaining snapshot that references it uniquely. The
            remaining snapshot's size will then show up as non zero. So
            deleting 0 sized snapshot is not as pointless as it might seem.
            It also means we have to loop through this, each snapshot set
            at a time and observe the before and after results. Perhaps
            better way exists...."""

            # Start with the oldest first
            snapname = snapshots.pop()
            snapshot = zfs.Snapshot(snapname)
            # It would be nicer, for performance purposes, to delete sets
            # of snapshots recursively but this might destroy more data than
            # absolutely necessary, plus the previous purging of zero sized
            # snapshots can easily break the recursion chain between
            # filesystems.
            # On the positive side there should be fewer snapshots and they
            # will mostly non-zero so we should get more effectiveness as a
            # result of deleting snapshots since they should be nearly always
            # non zero sized.
            util.debug("Destroying %s" % snapname, self.verbose)
            try:
                snapshot.destroy()
            except RuntimeError as message:
                # Would be nice to be able to mark service as degraded here
                # but it's better to try to continue on rather than to give
                # up alltogether (SMF maintenance state)
                sys.stderr.write("Warning: Cleanup failed to destroy: %s\n" % \
                                 (snapshot.name))
                sys.stderr.write("Details:\n%s\n" % (str(message)))
            else:
                self._destroyedsnaps.append(snapname)
            # Give zfs some time to recalculate.
            time.sleep(3)
Beispiel #5
0
    def _prune_snapshots(self, dataset, schedule):
        """Cleans out zero sized snapshots, kind of cautiously"""
        # Per schedule: We want to delete 0 sized
        # snapshots but we need to keep at least one around (the most
        # recent one) for each schedule so that that overlap is
        # maintained from frequent -> hourly -> daily etc.
        # Start off with the smallest interval schedule first and
        # move up. This increases the amount of data retained where
        # several snapshots are taken together like a frequent hourly
        # and daily snapshot taken at 12:00am. If 3 snapshots are all
        # identical and reference the same identical data they will all
        # be initially reported as zero for used size. Deleting the
        # daily first then the hourly would shift make the data referenced
        # by all 3 snapshots unique to the frequent scheduled snapshot.
        # This snapshot would probably be purged within an how ever and the
        # data referenced by it would be gone for good.
        # Doing it the other way however ensures that the data should
        # remain accessible to the user for at least a week as long as
        # the pool doesn't run low on available space before that.

        try:
            snaps = dataset.list_snapshots("%s%s" % (self._prefix, schedule))
            # Clone the list because we want to remove items from it
            # while iterating through it.
            remainingsnaps = snaps[:]
        except RuntimeError as message:
            sys.stderr.write(
                "Failed to list snapshots during snapshot cleanup\n")
            self.exitCode = smf.SMF_EXIT_ERR_FATAL
            raise RuntimeError(message)

        if (self._keepEmpties == False):
            try:  # remove the newest one from the list.
                snaps.pop()
            except IndexError:
                pass
            for snapname in snaps:
                try:
                    snapshot = zfs.Snapshot(snapname)
                except Exception as message:
                    sys.stderr.write(str(message))
                    # Not fatal, just skip to the next snapshot
                    continue

                try:
                    if snapshot.get_used_size() == 0:
                        util.debug("Destroying zero sized: " + snapname, \
                                   self.verbose)
                        try:
                            snapshot.destroy()
                        except RuntimeError as message:
                            sys.stderr.write("Failed to destroy snapshot: " +
                                             snapname + "\n")
                            self.exitCode = smf.SMF_EXIT_MON_DEGRADE
                            # Propogate exception so thread can exit
                            raise RuntimeError(message)
                        remainingsnaps.remove(snapname)
                except RuntimeError as message:
                    sys.stderr.write("Can not determine used size of: " + \
                                     snapname + "\n")
                    self.exitCode = smf.SMF_EXIT_MON_DEGRADE
                    #Propogate the exception to the thead run() method
                    raise RuntimeError(message)

        # Deleting individual snapshots instead of recursive sets
        # breaks the recursion chain and leaves child snapshots
        # dangling so we need to take care of cleaning up the
        # snapshots.
        target = len(remainingsnaps) - self._keep[schedule]
        counter = 0
        while counter < target:
            util.debug("Destroy expired snapshot: " + \
                       remainingsnaps[counter],
                       self.verbose)
            try:
                snapshot = zfs.Snapshot(remainingsnaps[counter])
            except Exception as message:
                sys.stderr.write(str(message))
                # Not fatal, just skip to the next snapshot
                counter += 1
                continue
            try:
                snapshot.destroy()
            except RuntimeError as message:
                sys.stderr.write("Failed to destroy snapshot: " +
                                 snapshot.name + "\n")
                self.exitCode = smf.SMF_EXIT_ERR_FATAL
                # Propogate exception so thread can exit
                raise RuntimeError(message)
            else:
                counter += 1
Beispiel #6
0
    def initialise_view(self):
        if len(self.shortcircuit) == 0:
            # Set TreeViews
            self.liststorefs = Gtk.ListStore(str, str, str, str, str, int,
                                             GObject.TYPE_PYOBJECT)
            list_filter = self.liststorefs.filter_new()
            list_sort = Gtk.TreeModelSort(list_filter)
            list_sort.set_sort_column_id(1, Gtk.SortType.ASCENDING)

            self.snaptreeview = self.builder.get_object("snaplist")
            self.snaptreeview.set_model(self.liststorefs)
            self.snaptreeview.get_selection().set_mode(
                Gtk.SelectionMode.MULTIPLE)

            cell0 = Gtk.CellRendererText()
            cell1 = Gtk.CellRendererText()
            cell2 = Gtk.CellRendererText()
            cell3 = Gtk.CellRendererText()
            cell4 = Gtk.CellRendererText()
            cell5 = Gtk.CellRendererText()

            typecol = Gtk.TreeViewColumn(_("Type"), cell0, text=0)
            typecol.set_sort_column_id(0)
            typecol.set_resizable(True)
            typecol.connect("clicked", self.__on_treeviewcol_clicked, 0)
            self.snaptreeview.append_column(typecol)

            mountptcol = Gtk.TreeViewColumn(_("Mount Point"), cell1, text=1)
            mountptcol.set_sort_column_id(1)
            mountptcol.set_resizable(True)
            mountptcol.connect("clicked", self.__on_treeviewcol_clicked, 1)
            self.snaptreeview.append_column(mountptcol)

            fsnamecol = Gtk.TreeViewColumn(_("File System Name"),
                                           cell2,
                                           text=2)
            fsnamecol.set_sort_column_id(2)
            fsnamecol.set_resizable(True)
            fsnamecol.connect("clicked", self.__on_treeviewcol_clicked, 2)
            self.snaptreeview.append_column(fsnamecol)

            snaplabelcol = Gtk.TreeViewColumn(_("Snapshot Name"),
                                              cell3,
                                              text=3)
            snaplabelcol.set_sort_column_id(3)
            snaplabelcol.set_resizable(True)
            snaplabelcol.connect("clicked", self.__on_treeviewcol_clicked, 3)
            self.snaptreeview.append_column(snaplabelcol)

            cell4.props.xalign = 1.0
            creationcol = Gtk.TreeViewColumn(_("Creation Time"), cell4, text=4)
            creationcol.set_sort_column_id(5)
            creationcol.set_resizable(True)
            creationcol.connect("clicked", self.__on_treeviewcol_clicked, 5)
            self.snaptreeview.append_column(creationcol)

            # Note to developers.
            # The second element is for internal matching and should not
            # be i18ned under any circumstances.
            typestore = Gtk.ListStore(str, str)
            typestore.append([_("All"), "All"])
            typestore.append([_("Backups"), "Backup"])
            typestore.append([_("Snapshots"), "Snapshot"])

            self.typefiltercombo = self.builder.get_object("typefiltercombo")
            self.typefiltercombo.set_model(typestore)
            typefiltercomboCell = Gtk.CellRendererText()
            self.typefiltercombo.pack_start(typefiltercomboCell, True)
            self.typefiltercombo.add_attribute(typefiltercomboCell, 'text', 0)

            # Note to developers.
            # The second element is for internal matching and should not
            # be i18ned under any circumstances.
            fsstore = Gtk.ListStore(str, str)
            fslist = self.datasets.list_filesystems()
            fsstore.append([_("All"), None])
            for fsname, fsmount in fslist:
                fsstore.append([fsname, fsname])
            self.fsfilterentry = self.builder.get_object("fsfilterentry")
            self.fsfilterentry.set_model(fsstore)
            self.fsfilterentry.set_entry_text_column(0)
            fsfilterentryCell = Gtk.CellRendererText()
            self.fsfilterentry.pack_start(fsfilterentryCell, False)

            schedstore = Gtk.ListStore(str, str)
            # Note to developers.
            # The second element is for internal matching and should not
            # be i18ned under any circumstances.
            schedstore.append([_("All"), None])
            schedstore.append([_("Monthly"), "monthly"])
            schedstore.append([_("Weekly"), "weekly"])
            schedstore.append([_("Daily"), "daily"])
            schedstore.append([_("Hourly"), "hourly"])
            schedstore.append([_("1/4 Hourly"), "frequent"])
            self.schedfilterentry = self.builder.get_object("schedfilterentry")
            self.schedfilterentry.set_model(schedstore)
            self.schedfilterentry.set_entry_text_column(0)
            schedentryCell = Gtk.CellRendererText()
            self.schedfilterentry.pack_start(schedentryCell, False)

            self.schedfilterentry.set_active(0)
            self.fsfilterentry.set_active(0)
            self.typefiltercombo.set_active(0)
        else:
            cloned = self.datasets.list_cloned_snapshots()
            num_snap = 0
            num_rsync = 0
            for snapname in self.shortcircuit:
                # Filter out snapshots that are the root
                # of cloned filesystems or volumes
                try:
                    cloned.index(snapname)
                    dialog = Gtk.MessageDialog(
                        None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE,
                        _("Snapshot can not be deleted"))
                    text = _("%s has one or more dependent clones "
                             "and will not be deleted. To delete "
                             "this snapshot, first delete all "
                             "datasets and snapshots cloned from "
                             "this snapshot.") \
                             % snapname
                    dialog.format_secondary_text(text)
                    dialog.run()
                    sys.exit(1)
                except ValueError:
                    path = os.path.abspath(snapname)
                    if not os.path.exists(path):
                        snapshot = zfs.Snapshot(snapname)
                        self.backuptodelete.append(snapshot)
                        num_snap += 1
                    else:
                        self.backuptodelete.append(RsyncBackup(snapname))
                        num_rsync += 1

            confirm = self.builder.get_object("confirmdialog")
            summary = self.builder.get_object("summarylabel")
            total = len(self.backuptodelete)

            text = ""
            if num_rsync != 0:
                if num_rsync == 1:
                    text = _("1 external backup will be deleted.")
                else:
                    text = _(
                        "%d external backups will be deleted.") % num_rsync

            if num_snap != 0:
                if len(text) != 0:
                    text += "\n"
                if num_snap == 1:
                    text += _("1 snapshot will be deleted.")
                else:
                    text += _("%d snapshots will be deleted.") % num_snap

            summary.set_text(text)
            response = confirm.run()
            if response != 2:
                sys.exit(0)
            else:
                # Create the thread in an idle loop in order to
                # avoid deadlock inside gtk.
                GLib.idle_add(self.__init_delete)
        return False