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!

    .. versionchanged:: 2018.3.0

        switched to localtime from gmtime so times now take into account timezones.

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!

    .. versionchanged:: 2018.3.0

        switched to localtime from gmtime so times now take into account timezones.

    '''
    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 __utils__['zfs.is_dataset'](name):
        ret['result'] = False
        ret['comment'] = 'invalid dataset name: {0}'.format(name)

    if not __salt__['zfs.exists'](name, **{'type': 'filesystem,volume'}):
        ret['comment'] = 'dataset {0} does not exist'.format(name)
        ret['result'] = False

    ## NOTE: prefix must be 4 or longer
    if not prefix or len(prefix) < 4:
        ret['comment'] = 'prefix ({0}) must be at least 4 long'.format(prefix)
        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 {0} 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'] = 'error creating snapshot ({0})'.format(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 ({0}) to snapshot ({1})".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 ({0}) to snapshot ({1})".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