Source code for saltext.zfs.states.zfs

"""
Manage ZFS datasets statefully.

Example
-------

.. code-block:: yaml

    test/shares/yuki:
      zfs.filesystem_present:
        - create_parent: true
        - properties:
            quota: 16G

    test/iscsi/haruhi:
      zfs.volume_present:
        - create_parent: true
        - volume_size: 16M
        - sparse: true
        - properties:
            readonly: on

    test/shares/yuki@frozen:
      zfs.snapshot_present

    moka_origin:
      zfs.hold_present:
        - snapshot: test/shares/yuki@frozen

    test/shares/moka:
      zfs.filesystem_present:
        - cloned_from: test/shares/yuki@frozen

    test/shares/moka@tsukune:
      zfs.snapshot_absent
"""

import logging
from datetime import datetime

from salt.utils.odict import OrderedDict

import saltext.zfs.utils.zfs

log = logging.getLogger(__name__)

__virtualname__ = "zfs"

# Compare modifiers for zfs.schedule_snapshot
comp_hour = {"minute": 0}
comp_day = {"minute": 0, "hour": 0}
comp_month = {"minute": 0, "hour": 0, "day": 1}
comp_year = {"minute": 0, "hour": 0, "day": 1, "month": 1}


def __virtual__():
    if not __grains__.get("zfs_support"):
        return False, "The zfs state cannot be loaded: zfs not supported"
    return __virtualname__


def _absent(name, dataset_type, force=False, recursive=False):
    """
    internal shared function for *_absent

    name : string
        name of dataset
    dataset_type : string [filesystem, volume, snapshot, or bookmark]
        type of dataset to remove
    force : boolean
        try harder to destroy the dataset
    recursive : boolean
        also destroy all the child datasets

    """
    ret = {"name": name, "changes": {}, "result": True, "comment": ""}

    ## log configuration
    dataset_type = dataset_type.lower()
    log.debug("zfs.%s_absent::%s::config::force = %s", dataset_type, name, force)
    log.debug("zfs.%s_absent::%s::config::recursive = %s", dataset_type, name, recursive)

    ## destroy dataset if needed
    if __salt__["zfs.exists"](name, **{"type": dataset_type}):
        ## NOTE: dataset found with the name and dataset_type
        if not __opts__["test"]:
            mod_res = __salt__["zfs.destroy"](name, **{"force": force, "recursive": recursive})
        else:
            mod_res = OrderedDict([("destroyed", True)])

        ret["result"] = mod_res["destroyed"]
        if ret["result"]:
            ret["changes"][name] = "destroyed"
            ret["comment"] = "{} {} was destroyed".format(
                dataset_type,
                name,
            )
        else:
            ret["comment"] = "failed to destroy {} {}".format(
                dataset_type,
                name,
            )
            if "error" in mod_res:
                ret["comment"] = mod_res["error"]
    else:
        ## NOTE: no dataset found with name of the dataset_type
        ret["comment"] = f"{dataset_type} {name} is absent"

    return ret


[docs] def filesystem_absent(name, force=False, recursive=False): """ Ensure a filesystem is absent from the system. name : string name of filesystem force : boolean try harder to destroy the dataset (zfs destroy -f) recursive : boolean also destroy all the child datasets (zfs destroy -r) .. warning:: If a volume with ``name`` exists, this state will succeed without destroying the volume specified by ``name``. This module is dataset type sensitive. """ if not saltext.zfs.utils.zfs.is_dataset(name): ret = { "name": name, "changes": {}, "result": False, "comment": f"invalid dataset name: {name}", } else: ret = _absent(name, "filesystem", force, recursive) return ret
[docs] def volume_absent(name, force=False, recursive=False): """ Ensure a volume is absent from the system. name : string name of volume force : boolean try harder to destroy the dataset (zfs destroy -f) recursive : boolean also destroy all the child datasets (zfs destroy -r) .. warning:: If a filesystem with ``name`` exists, this state will succeed without destroying the filesystem specified by ``name``. This module is dataset type sensitive. """ if not saltext.zfs.utils.zfs.is_dataset(name): ret = { "name": name, "changes": {}, "result": False, "comment": f"invalid dataset name: {name}", } else: ret = _absent(name, "volume", force, recursive) return ret
[docs] def snapshot_absent(name, force=False, recursive=False): """ Ensure a snapshot is absent from the system. name : string name of snapshot force : boolean try harder to destroy the dataset (zfs destroy -f) recursive : boolean also destroy all the child datasets (zfs destroy -r) """ if not saltext.zfs.utils.zfs.is_snapshot(name): ret = { "name": name, "changes": {}, "result": False, "comment": f"invalid snapshot name: {name}", } else: ret = _absent(name, "snapshot", force, recursive) return ret
[docs] def bookmark_absent(name, force=False, recursive=False): """ Ensure a bookmark is absent from the system. name : string name of snapshot force : boolean try harder to destroy the dataset (zfs destroy -f) recursive : boolean also destroy all the child datasets (zfs destroy -r) """ if not saltext.zfs.utils.zfs.is_bookmark(name): ret = { "name": name, "changes": {}, "result": False, "comment": f"invalid bookmark name: {name}", } else: ret = _absent(name, "bookmark", force, recursive) return ret
[docs] def hold_absent(name, snapshot, recursive=False): """ Ensure a hold is absent from the system. name : string name of hold snapshot : string name of snapshot recursive : boolean recursively releases a hold with the given tag on the snapshots of all descendent filesystems. """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} ## log configuration log.debug("zfs.hold_absent::%s::config::snapshot = %s", name, snapshot) log.debug("zfs.hold_absent::%s::config::recursive = %s", name, recursive) ## check we have a snapshot/tag name if not saltext.zfs.utils.zfs.is_snapshot(snapshot): ret["result"] = False ret["comment"] = f"invalid snapshot name: {snapshot}" return ret if ( saltext.zfs.utils.zfs.is_snapshot(name) or saltext.zfs.utils.zfs.is_bookmark(name) or name == "error" ): ret["result"] = False ret["comment"] = f"invalid tag name: {name}" return ret ## release hold if required holds = __salt__["zfs.holds"](snapshot) if name in holds: ## NOTE: hold found for snapshot, release it if not __opts__["test"]: mod_res = __salt__["zfs.release"](name, snapshot, **{"recursive": recursive}) else: mod_res = OrderedDict([("released", True)]) ret["result"] = mod_res["released"] if ret["result"]: ret["changes"] = {snapshot: {name: "released"}} ret["comment"] = "hold {} released".format( name, ) else: ret["comment"] = "failed to release hold {}".format( name, ) if "error" in mod_res: ret["comment"] = mod_res["error"] elif "error" in holds: ## NOTE: we have an error ret["result"] = False ret["comment"] = holds["error"] else: ## NOTE: no hold found with name for snapshot ret["comment"] = "hold {} is absent".format( name, ) return ret
[docs] def hold_present(name, snapshot, recursive=False): """ Ensure a hold is present on the system. name : string name of holdt snapshot : string name of snapshot recursive : boolean recursively add hold with the given tag on the snapshots of all descendent filesystems. """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} ## log configuration log.debug("zfs.hold_present::%s::config::snapshot = %s", name, snapshot) log.debug("zfs.hold_present::%s::config::recursive = %s", name, recursive) ## check we have a snapshot/tag name if not saltext.zfs.utils.zfs.is_snapshot(snapshot): ret["result"] = False ret["comment"] = f"invalid snapshot name: {snapshot}" return ret if ( saltext.zfs.utils.zfs.is_snapshot(name) or saltext.zfs.utils.zfs.is_bookmark(name) or name == "error" ): ret["result"] = False ret["comment"] = f"invalid tag name: {name}" return ret ## place hold if required holds = __salt__["zfs.holds"](snapshot) if name in holds: ## NOTE: hold with name already exists for snapshot ret["comment"] = "hold {} is present for {}".format( name, snapshot, ) else: ## NOTE: no hold found with name for snapshot if not __opts__["test"]: mod_res = __salt__["zfs.hold"](name, snapshot, **{"recursive": recursive}) else: mod_res = OrderedDict([("held", True)]) ret["result"] = mod_res["held"] if ret["result"]: ret["changes"] = OrderedDict([(snapshot, OrderedDict([(name, "held")]))]) ret["comment"] = f"hold {name} added to {snapshot}" else: ret["comment"] = f"failed to add hold {name} to {snapshot}" if "error" in mod_res: ret["comment"] = mod_res["error"] return ret
def _dataset_present( dataset_type, name, properties, volume_size=None, sparse=False, create_parent=False, cloned_from=None, ): """ internal handler for filesystem_present/volume_present dataset_type : string volume or filesystem name : string name of volume volume_size : string size of volume sparse : boolean create sparse volume create_parent : boolean creates all the non-existing parent datasets. any property specified on the command line using the -o option is ignored. cloned_from : string name of snapshot to clone properties : dict additional zfs properties (-o) .. note:: ``cloned_from`` is only use if the volume does not exist yet, when ``cloned_from`` is set after the volume exists it will be ignored. .. note:: Properties do not get cloned, if you specify the properties in the state file they will be applied on a subsequent run. ``volume_size`` is considered a property, so the volume's size will be corrected when the properties get updated if it differs from the original volume. The sparse parameter is ignored when using ``cloned_from``. """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} ## fallback dataset_type to filesystem if out of range if dataset_type not in ["filesystem", "volume"]: dataset_type = "filesystem" ## ensure properties are zfs values if properties is None: properties = {} properties = saltext.zfs.utils.zfs.from_auto_dict(properties) if volume_size: ## NOTE: add volsize to properties volume_size = saltext.zfs.utils.zfs.from_size(volume_size) properties.update({"volsize": volume_size}) # The sorted isn't necessary for proper behavior, but it helps for the unit # tests. propnames = ",".join(sorted(properties.keys())) ## log configuration log.debug("zfs.%s_present::%s::config::volume_size = %s", dataset_type, name, volume_size) log.debug("zfs.%s_present::%s::config::sparse = %s", dataset_type, name, sparse) log.debug( "zfs.%s_present::%s::config::create_parent = %s", dataset_type, name, create_parent, ) log.debug("zfs.%s_present::%s::config::cloned_from = %s", dataset_type, name, cloned_from) log.debug("zfs.%s_present::%s::config::properties = %s", dataset_type, name, properties) ## check we have valid filesystem name/volume name/clone snapshot if not saltext.zfs.utils.zfs.is_dataset(name): ret["result"] = False ret["comment"] = f"invalid dataset name: {name}" return ret if cloned_from and not saltext.zfs.utils.zfs.is_snapshot(cloned_from): ret["result"] = False ret["comment"] = f"{cloned_from} is not a snapshot" return ret ## ensure dataset is in correct state ## NOTE: update the dataset exists = __salt__["zfs.exists"](name, **{"type": dataset_type}) if exists and len(properties) == 0: ret["comment"] = f"{dataset_type} {name} is uptodate" elif exists and len(properties) > 0: ## NOTE: fetch current volume properties properties_current = __salt__["zfs.get"]( name, properties=propnames, type=dataset_type, fields="value", depth=0, parsable=True, ).get(name, OrderedDict()) ## NOTE: build list of properties to update properties_update = [] for prop in properties: ## NOTE: skip unexisting properties if prop not in properties_current: log.warning( "zfs.%s_present::%s::update - unknown property: %s", dataset_type, name, prop, ) continue ## NOTE: compare current and wanted value if properties_current[prop]["value"] != properties[prop]: properties_update.append(prop) ## NOTE: update pool properties for prop in properties_update: if not __opts__["test"]: mod_res = __salt__["zfs.set"](name, **{prop: properties[prop]}) else: mod_res = OrderedDict([("set", True)]) if mod_res["set"]: if name not in ret["changes"]: ret["changes"][name] = {} ret["changes"][name][prop] = properties[prop] else: ret["result"] = False if ret["comment"] == "": ret["comment"] = "The following properties were not updated:" ret["comment"] = "{} {}".format(ret["comment"], prop) ## NOTE: update comment if ret["result"] and name in ret["changes"]: ret["comment"] = f"{dataset_type} {name} was updated" elif ret["result"]: ret["comment"] = f"{dataset_type} {name} is uptodate" else: ret["comment"] = f"{dataset_type} {name} failed to be updated" ## NOTE: create or clone the dataset elif not exists: mod_res_action = "cloned" if cloned_from else "created" if __opts__["test"]: ## NOTE: pretend to create/clone mod_res = OrderedDict([(mod_res_action, True)]) elif cloned_from: ## NOTE: add volsize to properties if volume_size: properties["volsize"] = volume_size ## NOTE: clone the dataset mod_res = __salt__["zfs.clone"]( cloned_from, name, **{"create_parent": create_parent, "properties": properties}, ) else: ## NOTE: create the dataset mod_res = __salt__["zfs.create"]( name, **{ "create_parent": create_parent, "properties": properties, "volume_size": volume_size, "sparse": sparse, }, ) ret["result"] = mod_res[mod_res_action] if ret["result"]: ret["changes"][name] = mod_res_action if properties: ret["changes"][name] = properties ret["comment"] = "{} {} was {}".format( dataset_type, name, mod_res_action, ) else: ret["comment"] = "failed to {} {} {}".format( mod_res_action[:-1], dataset_type, name, ) if "error" in mod_res: ret["comment"] = mod_res["error"] return ret
[docs] def filesystem_present(name, create_parent=False, properties=None, cloned_from=None): """ Ensure a filesystem exists and has properties set. name : string name of filesystem create_parent : boolean creates all the non-existing parent datasets. any property specified on the command line using the -o option is ignored. cloned_from : string name of snapshot to clone properties : dict additional zfs properties (-o) .. note:: ``cloned_from`` is only use if the filesystem does not exist yet, when ``cloned_from`` is set after the filesystem exists it will be ignored. .. note:: Properties do not get cloned, if you specify the properties in the state file they will be applied on a subsequent run. """ return _dataset_present( "filesystem", name, properties, create_parent=create_parent, cloned_from=cloned_from, )
[docs] def volume_present( name, volume_size, sparse=False, create_parent=False, properties=None, cloned_from=None, ): """ Ensure a volume exists and has properties set. name : string name of volume volume_size : string size of volume sparse : boolean create sparse volume create_parent : boolean creates all the non-existing parent datasets. any property specified on the command line using the -o option is ignored. cloned_from : string name of snapshot to clone properties : dict additional zfs properties (-o) .. note:: ``cloned_from`` is only use if the volume does not exist yet, when ``cloned_from`` is set after the volume exists it will be ignored. .. note:: Properties do not get cloned, if you specify the properties in the state file they will be applied on a subsequent run. ``volume_size`` is considered a property, so the volume's size will be corrected when the properties get updated if it differs from the original volume. The sparse parameter is ignored when using ``cloned_from``. """ return _dataset_present( "volume", name, properties, volume_size, sparse=sparse, create_parent=create_parent, cloned_from=cloned_from, )
[docs] def bookmark_present(name, snapshot): """ Ensure a bookmark exists. name : string name of bookmark snapshot : string name of snapshot """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} ## log configuration log.debug("zfs.bookmark_present::%s::config::snapshot = %s", name, snapshot) ## check we have valid snapshot/bookmark name if not saltext.zfs.utils.zfs.is_snapshot(snapshot): ret["result"] = False ret["comment"] = f"invalid snapshot name: {name}" return ret if "#" not in name and "/" not in name: ## NOTE: simple snapshot name # take the snapshot name and replace the snapshot but with the simple name # e.g. pool/fs@snap + bm --> pool/fs#bm name = "{}#{}".format(snapshot[: snapshot.index("@")], name) ret["name"] = name if not saltext.zfs.utils.zfs.is_bookmark(name): ret["result"] = False ret["comment"] = f"invalid bookmark name: {name}" return ret ## ensure bookmark exists if not __salt__["zfs.exists"](name, **{"type": "bookmark"}): ## NOTE: bookmark the snapshot if not __opts__["test"]: mod_res = __salt__["zfs.bookmark"](snapshot, name) else: mod_res = OrderedDict([("bookmarked", True)]) ret["result"] = mod_res["bookmarked"] if ret["result"]: ret["changes"][name] = snapshot ret["comment"] = f"{snapshot} bookmarked as {name}" else: ret["comment"] = f"failed to bookmark {snapshot}" if "error" in mod_res: ret["comment"] = mod_res["error"] else: ## NOTE: bookmark already exists ret["comment"] = "bookmark is present" return ret
[docs] def snapshot_present(name, recursive=False, properties=None): """ Ensure a snapshot exists and has properties set. name : string name of snapshot recursive : boolean recursively create snapshots of all descendent datasets properties : dict additional zfs properties (-o) .. note: Properties are only set at creation time """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} ## log configuration log.debug("zfs.snapshot_present::%s::config::recursive = %s", name, recursive) log.debug("zfs.snapshot_present::%s::config::properties = %s", name, properties) ## ensure properties are zfs values if properties: properties = saltext.zfs.utils.zfs.from_auto_dict(properties) ## check we have valid snapshot name if not saltext.zfs.utils.zfs.is_snapshot(name): ret["result"] = False ret["comment"] = f"invalid snapshot name: {name}" return ret ## ensure snapshot exits if not __salt__["zfs.exists"](name, **{"type": "snapshot"}): ## NOTE: create the snapshot if not __opts__["test"]: mod_res = __salt__["zfs.snapshot"]( name, **{"recursive": recursive, "properties": properties} ) else: mod_res = OrderedDict([("snapshotted", True)]) ret["result"] = mod_res["snapshotted"] if ret["result"]: ret["changes"][name] = "snapshotted" if properties: ret["changes"][name] = properties ret["comment"] = f"snapshot {name} was created" else: ret["comment"] = f"failed to create snapshot {name}" if "error" in mod_res: ret["comment"] = mod_res["error"] else: ## NOTE: snapshot already exists ret["comment"] = "snapshot is present" return ret
def _schedule_snapshot_retrieve(dataset, prefix, snapshots): """ Update snapshots dict with current snapshots dataset: string name of filesystem or volume prefix : string prefix for the snapshots e.g. 'test' will result in snapshots being named 'test-yyyymmdd_hhmm' snapshots : OrderedDict preseeded OrderedDict with configuration """ ## NOTE: retrieve all snapshots for the dataset for snap in sorted( __salt__["zfs.list"](dataset, **{"recursive": True, "depth": 1, "type": "snapshot"}).keys() ): ## NOTE: we only want the actualy name ## myzpool/data@zbck-20171201_000248 -> zbck-20171201_000248 snap_name = snap[snap.index("@") + 1 :] ## NOTE: we only want snapshots matching our prefix if not snap_name.startswith(f"{prefix}-"): continue ## NOTE: retrieve the holds for this snapshot snap_holds = __salt__["zfs.holds"](snap) ## NOTE: this snapshot has no holds, eligable for pruning if not snap_holds: snapshots["_prunable"].append(snap) ## NOTE: update snapshots based on holds (if any) ## we are only interested in the ones from our schedule ## if we find any others we skip them for hold in snap_holds: if hold in snapshots["_schedule"].keys(): snapshots[hold].append(snap) return snapshots def _schedule_snapshot_prepare(dataset, prefix, snapshots): """ Update snapshots dict with info for a new snapshot dataset: string name of filesystem or volume prefix : string prefix for the snapshots e.g. 'test' will result in snapshots being named 'test-yyyymmdd_hhmm' snapshots : OrderedDict preseeded OrderedDict with configuration """ ## NOTE: generate new snapshot name snapshot_create_name = "{dataset}@{prefix}-{timestamp}".format( dataset=dataset, prefix=prefix, timestamp=datetime.now().strftime("%Y%m%d_%H%M%S"), ) ## NOTE: figure out if we need to create the snapshot timestamp_now = datetime.now().replace(second=0, microsecond=0) snapshots["_create"][snapshot_create_name] = [] for hold, hold_count in snapshots["_schedule"].items(): ## NOTE: skip hold if we don't keep snapshots for it if hold_count == 0: continue ## NOTE: figure out if we need the current hold on the new snapshot if snapshots[hold]: ## NOTE: extract datetime from snapshot name timestamp = datetime.strptime( snapshots[hold][-1], f"{dataset}@{prefix}-%Y%m%d_%H%M%S", ).replace(second=0, microsecond=0) ## NOTE: compare current timestamp to timestamp from snapshot if hold == "minute" and timestamp_now <= timestamp: continue if hold == "hour" and timestamp_now.replace(**comp_hour) <= timestamp.replace( **comp_hour ): continue if hold == "day" and timestamp_now.replace(**comp_day) <= timestamp.replace(**comp_day): continue if hold == "month" and timestamp_now.replace(**comp_month) <= timestamp.replace( **comp_month ): continue if hold == "year" and timestamp_now.replace(**comp_year) <= timestamp.replace( **comp_year ): continue ## NOTE: add hold entry for snapshot snapshots["_create"][snapshot_create_name].append(hold) return snapshots
[docs] def scheduled_snapshot(name, prefix, recursive=True, schedule=None): """ Maintain a set of snapshots based on a schedule. name : string name of filesystem or volume prefix : string prefix for the snapshots e.g. 'test' will result in snapshots being named 'test-yyyymmdd_hhmm' recursive : boolean create snapshots for all children also schedule : dict dict holding the schedule, the following keys are available (minute, hour, day, month, and year) by default all are set to 0 the value indicated the number of snapshots of that type to keep around. .. warning:: snapshots will only be created and pruned every time the state runs. a schedule must be setup to automatically run the state. this means that if you run the state daily the hourly snapshot will only be made once per day! """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} ## initialize defaults schedule_holds = ["minute", "hour", "day", "month", "year"] snapshots = OrderedDict( [("_create", OrderedDict()), ("_prunable", []), ("_schedule", OrderedDict())] ) ## strict configuration validation ## NOTE: we need a valid dataset if not saltext.zfs.utils.zfs.is_dataset(name): ret["result"] = False ret["comment"] = f"invalid dataset name: {name}" if not __salt__["zfs.exists"](name, **{"type": "filesystem,volume"}): ret["comment"] = f"dataset {name} does not exist" ret["result"] = False ## NOTE: prefix must be 4 or longer if not prefix or len(prefix) < 4: ret["comment"] = f"prefix ({prefix}) must be at least 4 long" ret["result"] = False ## NOTE: validate schedule total_count = 0 for hold in schedule_holds: snapshots[hold] = [] if hold not in schedule: snapshots["_schedule"][hold] = 0 elif isinstance(schedule[hold], int): snapshots["_schedule"][hold] = schedule[hold] else: ret["result"] = False ret["comment"] = "schedule value for {} is not an integer".format( hold, ) break total_count += snapshots["_schedule"][hold] if ret["result"] and total_count == 0: ret["result"] = False ret["comment"] = "schedule is not valid, you need to keep atleast 1 snapshot" ## NOTE: return if configuration is not valid if not ret["result"]: return ret ## retrieve existing snapshots snapshots = _schedule_snapshot_retrieve(name, prefix, snapshots) ## prepare snapshot snapshots = _schedule_snapshot_prepare(name, prefix, snapshots) ## log configuration log.debug("zfs.scheduled_snapshot::%s::config::recursive = %s", name, recursive) log.debug("zfs.scheduled_snapshot::%s::config::prefix = %s", name, prefix) log.debug("zfs.scheduled_snapshot::%s::snapshots = %s", name, snapshots) ## create snapshot(s) for snapshot_name, snapshot_holds in snapshots["_create"].items(): ## NOTE: skip if new snapshot has no holds if not snapshot_holds: continue ## NOTE: create snapshot if not __opts__["test"]: mod_res = __salt__["zfs.snapshot"](snapshot_name, **{"recursive": recursive}) else: mod_res = OrderedDict([("snapshotted", True)]) if not mod_res["snapshotted"]: ret["result"] = False ret["comment"] = f"error creating snapshot ({snapshot_name})" else: ## NOTE: create holds (if we have a snapshot) for hold in snapshot_holds: if not __opts__["test"]: mod_res = __salt__["zfs.hold"](hold, snapshot_name, **{"recursive": recursive}) else: mod_res = OrderedDict([("held", True)]) if not mod_res["held"]: ret["result"] = False ret["comment"] = "error adding hold ({}) to snapshot ({})".format( hold, snapshot_name, ) break snapshots[hold].append(snapshot_name) if ret["result"]: ret["comment"] = "scheduled snapshots updated" if "created" not in ret["changes"]: ret["changes"]["created"] = [] ret["changes"]["created"].append(snapshot_name) ## prune hold(s) for hold, hold_count in snapshots["_schedule"].items(): while ret["result"] and len(snapshots[hold]) > hold_count: ## NOTE: pop oldest snapshot snapshot_name = snapshots[hold].pop(0) ## NOTE: release hold for snapshot if not __opts__["test"]: mod_res = __salt__["zfs.release"](hold, snapshot_name, **{"recursive": recursive}) else: mod_res = OrderedDict([("released", True)]) if not mod_res["released"]: ret["result"] = False ret["comment"] = "error adding hold ({}) to snapshot ({})".format( hold, snapshot_name, ) ## NOTE: mark as prunable if not __salt__["zfs.holds"](snapshot_name): snapshots["_prunable"].append(snapshot_name) ## prune snapshot(s) for snapshot_name in snapshots["_prunable"]: ## NOTE: destroy snapshot if not __opts__["test"]: mod_res = __salt__["zfs.destroy"](snapshot_name, **{"recursive": recursive}) else: mod_res = OrderedDict([("destroyed", True)]) if not mod_res["destroyed"]: ret["result"] = False ret["comment"] = "error prunding snapshot ({1})".format( snapshot_name, ) break if ret["result"] and snapshots["_prunable"]: ret["comment"] = "scheduled snapshots updated" ret["changes"]["pruned"] = snapshots["_prunable"] if ret["result"] and not ret["changes"]: ret["comment"] = "scheduled snapshots are up to date" return ret