def sync_datasets(self, source_node, source_datasets, target_node,
                      filter_properties, set_properties):
        """Sync datasets, or thin-only on both sides"""

        fail_count = 0
        target_datasets = []
        for source_dataset in source_datasets:

            try:
                # determine corresponding target_dataset
                target_name = self.args.target_path + "/" + source_dataset.lstrip_path(
                    self.args.strip_path)
                target_dataset = ZfsDataset(target_node, target_name)
                target_datasets.append(target_dataset)

                # ensure parents exists
                # TODO: this isnt perfect yet, in some cases it can create parents when it shouldn't.
                if not self.args.no_send \
                        and target_dataset.parent not in target_datasets \
                        and not target_dataset.parent.exists:
                    target_dataset.parent.create_filesystem(parents=True)

                # determine common zpool features (cached, so no problem we call it often)
                source_features = source_node.get_zfs_pool(
                    source_dataset.split_path()[0]).features
                target_features = target_node.get_zfs_pool(
                    target_dataset.split_path()[0]).features
                common_features = source_features and target_features
                # source_dataset.debug("Common features: {}".format(common_features))

                source_dataset.sync_snapshots(
                    target_dataset,
                    show_progress=self.args.progress,
                    features=common_features,
                    filter_properties=filter_properties,
                    set_properties=set_properties,
                    ignore_recv_exit_code=self.args.ignore_transfer_errors,
                    holds=not self.args.no_holds,
                    rollback=self.args.rollback,
                    raw=self.args.raw,
                    also_other_snapshots=self.args.other_snapshots,
                    no_send=self.args.no_send,
                    destroy_incompatible=self.args.destroy_incompatible)
            except Exception as e:
                fail_count = fail_count + 1
                source_dataset.error("FAILED: " + str(e))
                if self.args.debug:
                    raise

        if not self.args.no_thinning:
            self.thin_missing_targets(target_dataset=ZfsDataset(
                target_node, self.args.target_path),
                                      used_target_datasets=target_datasets)

        if self.args.destroy_missing is not None:
            self.destroy_missing_targets(target_dataset=ZfsDataset(
                target_node, self.args.target_path),
                                         used_target_datasets=target_datasets)

        return fail_count
Exemple #2
0
    def selected_datasets(self):
        """determine filesystems that should be backupped by looking at the special autobackup-property, systemwide

           returns: list of ZfsDataset
        """

        self.debug("Getting selected datasets")

        # get all source filesystems that have the backup property
        lines = self.run(tab_split=True,
                         readonly=True,
                         cmd=[
                             "zfs", "get", "-t", "volume,filesystem", "-o",
                             "name,value,source", "-s", "local,inherited",
                             "-H", "autobackup:" + self.backup_name
                         ])

        # determine filesystems that should be actually backupped
        selected_filesystems = []
        direct_filesystems = []
        for line in lines:
            (name, value, source) = line
            dataset = ZfsDataset(self, name)

            if value == "false":
                dataset.verbose("Ignored (disabled)")

            else:
                if source == "local" and (value == "true" or value == "child"):
                    direct_filesystems.append(name)

                if source == "local" and value == "true":
                    dataset.verbose("Selected (direct selection)")
                    selected_filesystems.append(dataset)
                elif source.find("inherited from ") == 0 and (
                        value == "true" or value == "child"):
                    inherited_from = re.sub("^inherited from ", "", source)
                    if inherited_from in direct_filesystems:
                        selected_filesystems.append(dataset)
                        dataset.verbose("Selected (inherited selection)")
                    else:
                        dataset.debug("Ignored (already a backup)")
                else:
                    dataset.verbose("Ignored (only childs)")

        return selected_filesystems
Exemple #3
0
    def selected_datasets(self, ignore_received=True):
        """determine filesystems that should be backupped by looking at the special autobackup-property, systemwide

           returns: list of ZfsDataset
        """

        self.debug("Getting selected datasets")

        # get all source filesystems that have the backup property
        lines = self.run(tab_split=True,
                         readonly=True,
                         cmd=[
                             "zfs", "get", "-t", "volume,filesystem", "-o",
                             "name,value,source", "-H",
                             "autobackup:" + self.backup_name
                         ])

        # The returnlist of selected ZfsDataset's:
        selected_filesystems = []

        # list of sources, used to resolve inherited sources
        sources = {}

        for line in lines:
            (name, value, raw_source) = line
            dataset = ZfsDataset(self, name)

            # "resolve" inherited sources
            sources[name] = raw_source
            if raw_source.find("inherited from ") == 0:
                inherited = True
                inherited_from = re.sub("^inherited from ", "", raw_source)
                source = sources[inherited_from]
            else:
                inherited = False
                source = raw_source

            # determine it
            if dataset.is_selected(value=value,
                                   source=source,
                                   inherited=inherited,
                                   ignore_received=ignore_received):
                selected_filesystems.append(dataset)

        return selected_filesystems
Exemple #4
0
    def consistent_snapshot(self, datasets, snapshot_name, min_changed_bytes):
        """create a consistent (atomic) snapshot of specified datasets, per pool.
        """

        pools = {}

        # collect snapshots that we want to make, per pool
        # self.debug(datasets)
        for dataset in datasets:
            if not dataset.is_changed_ours(min_changed_bytes):
                dataset.verbose("No changes since {}".format(
                    dataset.our_snapshots[-1].snapshot_name))
                continue

            # force_exist, since we're making it
            snapshot = ZfsDataset(dataset.zfs_node,
                                  dataset.name + "@" + snapshot_name,
                                  force_exists=True)

            pool = dataset.split_path()[0]
            if pool not in pools:
                pools[pool] = []

            pools[pool].append(snapshot)

            # update cache, but try to prevent an unneeded zfs list
            if self.readonly or CachedProperty.is_cached(dataset, 'snapshots'):
                dataset.snapshots.append(
                    snapshot
                )  # NOTE: this will trigger zfs list if its not cached

        if not pools:
            self.verbose("No changes anywhere: not creating snapshots.")
            return

        # create consistent snapshot per pool
        for (pool_name, snapshots) in pools.items():
            cmd = ["zfs", "snapshot"]

            cmd.extend(map(lambda snapshot_: str(snapshot_), snapshots))

            self.verbose("Creating snapshots {} in pool {}".format(
                snapshot_name, pool_name))
            self.run(cmd, readonly=False)
    def run(self):

        try:
            self.verbose(self.HEADER)

            if self.args.test:
                self.verbose(
                    "TEST MODE - SIMULATING WITHOUT MAKING ANY CHANGES")

            self.set_title("Source settings")

            description = "[Source]"
            if self.args.no_thinning:
                source_thinner = None
            else:
                source_thinner = Thinner(self.args.keep_source)
            source_node = ZfsNode(self.args.backup_name,
                                  self,
                                  ssh_config=self.args.ssh_config,
                                  ssh_to=self.args.ssh_source,
                                  readonly=self.args.test,
                                  debug_output=self.args.debug_output,
                                  description=description,
                                  thinner=source_thinner)
            source_node.verbose(
                "Selects all datasets that have property 'autobackup:{}=true' (or childs of datasets that have "
                "'autobackup:{}=child')".format(self.args.backup_name,
                                                self.args.backup_name))

            self.set_title("Selecting")
            selected_source_datasets = source_node.selected_datasets
            if not selected_source_datasets:
                self.error(
                    "No source filesystems selected, please do a 'zfs set autobackup:{0}=true' on the source datasets "
                    "you want to select.".format(self.args.backup_name))
                return 255

            # filter out already replicated stuff?
            source_datasets = self.filter_replicated(selected_source_datasets)

            if not self.args.no_snapshot:
                self.set_title("Snapshotting")
                source_node.consistent_snapshot(
                    source_datasets,
                    source_node.new_snapshotname(),
                    min_changed_bytes=self.args.min_change)

            # if target is specified, we sync the datasets, otherwise we just thin the source. (e.g. snapshot mode)
            if self.args.target_path:

                # create target_node
                self.set_title("Target settings")
                if self.args.no_thinning:
                    target_thinner = None
                else:
                    target_thinner = Thinner(self.args.keep_target)
                target_node = ZfsNode(self.args.backup_name,
                                      self,
                                      ssh_config=self.args.ssh_config,
                                      ssh_to=self.args.ssh_target,
                                      readonly=self.args.test,
                                      debug_output=self.args.debug_output,
                                      description="[Target]",
                                      thinner=target_thinner)
                target_node.verbose("Receive datasets under: {}".format(
                    self.args.target_path))

                self.set_title("Synchronising")

                # check if exists, to prevent vague errors
                target_dataset = ZfsDataset(target_node, self.args.target_path)
                if not target_dataset.exists:
                    raise (Exception(
                        "Target path '{}' does not exist. Please create this dataset first."
                        .format(target_dataset)))

                # do the actual sync
                # NOTE: even with no_send, no_thinning and no_snapshot it does a usefull thing because it checks if the common snapshots and shows incompatible snapshots
                fail_count = self.sync_datasets(
                    source_node=source_node,
                    source_datasets=source_datasets,
                    target_node=target_node)

            #no target specified, run in snapshot-only mode
            else:
                self.thin_source(source_datasets)
                fail_count = 0

            if not fail_count:
                if self.args.test:
                    self.set_title("All tests successful.")
                else:
                    self.set_title("All operations completed successfully")
                    if not self.args.target_path:
                        self.verbose(
                            "(No target_path specified, only operated as snapshot tool.)"
                        )

            else:
                if fail_count != 255:
                    self.error("{} failures!".format(fail_count))

            if self.args.test:
                self.verbose("")
                self.verbose("TEST MODE - DID NOT MAKE ANY CHANGES!")

            return fail_count

        except Exception as e:
            self.error("Exception: " + str(e))
            if self.args.debug:
                raise
            return 255
        except KeyboardInterrupt:
            self.error("Aborted")
            return 255
    def run(self):

        try:
            self.verbose(self.HEADER)

            if self.args.test:
                self.verbose(
                    "TEST MODE - SIMULATING WITHOUT MAKING ANY CHANGES")

            self.set_title("Source settings")

            description = "[Source]"
            source_thinner = Thinner(self.args.keep_source)
            source_node = ZfsNode(self.args.backup_name,
                                  self,
                                  ssh_config=self.args.ssh_config,
                                  ssh_to=self.args.ssh_source,
                                  readonly=self.args.test,
                                  debug_output=self.args.debug_output,
                                  description=description,
                                  thinner=source_thinner)
            source_node.verbose(
                "Selects all datasets that have property 'autobackup:{}=true' (or childs of datasets that have "
                "'autobackup:{}=child')".format(self.args.backup_name,
                                                self.args.backup_name))

            self.set_title("Selecting")
            selected_source_datasets = source_node.selected_datasets
            if not selected_source_datasets:
                self.error(
                    "No source filesystems selected, please do a 'zfs set autobackup:{0}=true' on the source datasets "
                    "you want to select.".format(self.args.backup_name))
                return 255

            source_datasets = []

            # filter out already replicated stuff?
            if not self.args.ignore_replicated:
                source_datasets = selected_source_datasets
            else:
                self.set_title("Filtering already replicated filesystems")
                for selected_source_dataset in selected_source_datasets:
                    if selected_source_dataset.is_changed(
                            self.args.min_change):
                        source_datasets.append(selected_source_dataset)
                    else:
                        selected_source_dataset.verbose(
                            "Ignoring, already replicated")

            if not self.args.no_snapshot:
                self.set_title("Snapshotting")
                source_node.consistent_snapshot(
                    source_datasets,
                    source_node.new_snapshotname(),
                    min_changed_bytes=self.args.min_change)

            # if target is specified, we sync the datasets, otherwise we just thin the source. (e.g. snapshot mode)
            if self.args.target_path:

                # create target_node
                self.set_title("Target settings")
                target_thinner = Thinner(self.args.keep_target)
                target_node = ZfsNode(self.args.backup_name,
                                      self,
                                      ssh_config=self.args.ssh_config,
                                      ssh_to=self.args.ssh_target,
                                      readonly=self.args.test,
                                      debug_output=self.args.debug_output,
                                      description="[Target]",
                                      thinner=target_thinner)
                target_node.verbose("Receive datasets under: {}".format(
                    self.args.target_path))

                # determine filter- and set properties lists
                if self.args.filter_properties:
                    filter_properties = self.args.filter_properties.split(",")
                else:
                    filter_properties = []

                if self.args.set_properties:
                    set_properties = self.args.set_properties.split(",")
                else:
                    set_properties = []

                if self.args.clear_refreservation:
                    filter_properties.append("refreservation")

                if self.args.clear_mountpoint:
                    set_properties.append("canmount=noauto")

                if self.args.no_send:
                    self.set_title("Thinning source and target")
                else:
                    self.set_title("Sending and thinning")

                # check if exists, to prevent vague errors
                target_dataset = ZfsDataset(target_node, self.args.target_path)
                if not target_dataset.exists:
                    raise (Exception(
                        "Target path '{}' does not exist. Please create this dataset first."
                        .format(target_dataset)))

                # do the actual sync
                fail_count = self.sync_datasets(
                    source_node=source_node,
                    source_datasets=source_datasets,
                    target_node=target_node,
                    filter_properties=filter_properties,
                    set_properties=set_properties)

            else:
                if not self.args.no_thinning:
                    self.thin_source(source_datasets)
                fail_count = 0

            if not fail_count:
                if self.args.test:
                    self.set_title("All tests successfull.")
                else:
                    self.set_title("All operations completed successfully")
                    if not self.args.target_path:
                        self.verbose(
                            "(No target_path specified, only operated as snapshot tool.)"
                        )

            else:
                if fail_count != 255:
                    self.error("{} failures!".format(fail_count))

            if self.args.test:
                self.verbose("")
                self.verbose("TEST MODE - DID NOT MAKE ANY CHANGES!")

            return fail_count

        except Exception as e:
            self.error("Exception: " + str(e))
            if self.args.debug:
                raise
            return 255
        except KeyboardInterrupt:
            self.error("Aborted")
            return 255