Evaluate and execute the schedule

        :param datetime now: Override current time with a datetime object instance``

def eval(self, now=None):
        '''
        Evaluate and execute the schedule

        :param datetime now: Override current time with a datetime object instance``

        '''

        log.trace('==== evaluating schedule now %s =====', now)

        loop_interval = self.opts['loop_interval']
        if not isinstance(loop_interval, datetime.timedelta):
            loop_interval = datetime.timedelta(seconds=loop_interval)

        def _splay(splaytime):
            '''
            Calculate splaytime
            '''
            splay_ = None
            if isinstance(splaytime, dict):
                if splaytime['end'] >= splaytime['start']:
                    splay_ = random.randint(splaytime['start'],
                                            splaytime['end'])
                else:
                    log.error('schedule.handle_func: Invalid Splay, '
                              'end must be larger than start. Ignoring splay.')
            else:
                splay_ = random.randint(1, splaytime)
            return splay_

        def _handle_time_elements(data):
            '''
            Handle schedule item with time elements
            seconds, minutes, hours, days
            '''
            if '_seconds' not in data:
                interval = int(data.get('seconds', 0))
                interval += int(data.get('minutes', 0)) * 60
                interval += int(data.get('hours', 0)) * 3600
                interval += int(data.get('days', 0)) * 86400

                data['_seconds'] = interval

                if not data['_next_fire_time']:
                    data['_next_fire_time'] = now + datetime.timedelta(seconds=data['_seconds'])

                if interval < self.loop_interval:
                    self.loop_interval = interval

                data['_next_scheduled_fire_time'] = now + datetime.timedelta(seconds=data['_seconds'])

        def _handle_once(data, loop_interval):
            '''
            Handle schedule item with once
            '''
            if data['_next_fire_time']:
                if data['_next_fire_time'] < now - loop_interval or \
                   data['_next_fire_time'] > now and \
                   not data['_splay']:
                    data['_continue'] = True

            if not data['_next_fire_time'] and \
                    not data['_splay']:
                once = data['once']
                if not isinstance(once, datetime.datetime):
                    once_fmt = data.get('once_fmt', '%Y-%m-%dT%H:%M:%S')
                    try:
                        once = datetime.datetime.strptime(data['once'],
                                                          once_fmt)
                    except (TypeError, ValueError):
                        data['_error'] = ('Date string could not '
                                          'be parsed: {0}, {1}. '
                                          'Ignoring job {2}.'.format(
                                              data['once'],
                                              once_fmt,
                                              data['name']))
                        log.error(data['_error'])
                        return
                data['_next_fire_time'] = once
                data['_next_scheduled_fire_time'] = once
                # If _next_fire_time is less than now, continue
                if once < now - loop_interval:
                    data['_continue'] = True

        def _handle_when(data, loop_interval):
            '''
            Handle schedule item with when
            '''
            if not _WHEN_SUPPORTED:
                data['_error'] = ('Missing python-dateutil. '
                                  'Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])
                return

            if not isinstance(data['when'], list):
                _when_data = [data['when']]
            else:
                _when_data = data['when']

            _when = []
            for i in _when_data:
                if ('pillar' in self.opts and 'whens' in self.opts['pillar'] and
                        i in self.opts['pillar']['whens']):
                    if not isinstance(self.opts['pillar']['whens'],
                                      dict):
                        data['_error'] = ('Pillar item "whens" '
                                          'must be a dict. '
                                          'Ignoring job {0}.'.format(data['name']))
                        log.error(data['_error'])
                        return
                    when_ = self.opts['pillar']['whens'][i]
                elif ('whens' in self.opts['grains'] and
                      i in self.opts['grains']['whens']):
                    if not isinstance(self.opts['grains']['whens'],
                                      dict):
                        data['_error'] = ('Grain "whens" must be a dict. '
                                          'Ignoring job {0}.'.format(data['name']))
                        log.error(data['_error'])
                        return
                    when_ = self.opts['grains']['whens'][i]
                else:
                    when_ = i

                if not isinstance(when_, datetime.datetime):
                    try:
                        when_ = dateutil_parser.parse(when_)
                    except ValueError:
                        data['_error'] = ('Invalid date string {0}. '
                                          'Ignoring job {1}.'.format(i, data['name']))
                        log.error(data['_error'])
                        return

                _when.append(when_)

            if data['_splay']:
                _when.append(data['_splay'])

            # Sort the list of "whens" from earlier to later schedules
            _when.sort()

            # Copy the list so we can loop through it
            for i in copy.deepcopy(_when):
                if len(_when) > 1:
                    if i < now - loop_interval:
                        # Remove all missed schedules except the latest one.
                        # We need it to detect if it was triggered previously.
                        _when.remove(i)

            if _when:
                # Grab the first element, which is the next run time or
                # last scheduled time in the past.
                when = _when[0]

                if when < now - loop_interval and \
                        not data.get('_run', False) and \
                        not data.get('run', False) and \
                        not data['_splay']:
                    data['_next_fire_time'] = None
                    data['_continue'] = True
                    return

                if '_run' not in data:
                    # Prevent run of jobs from the past
                    data['_run'] = bool(when >= now - loop_interval)

                if not data['_next_fire_time']:
                    data['_next_fire_time'] = when

                data['_next_scheduled_fire_time'] = when

                if data['_next_fire_time'] < when and \
                        not run and \
                        not data['_run']:
                    data['_next_fire_time'] = when
                    data['_run'] = True

            elif not data.get('_run', False):
                data['_next_fire_time'] = None
                data['_continue'] = True

        def _handle_cron(data, loop_interval):
            '''
            Handle schedule item with cron
            '''
            if not _CRON_SUPPORTED:
                data['_error'] = ('Missing python-croniter. '
                                  'Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])
                return

            if data['_next_fire_time'] is None:
                # Get next time frame for a "cron" job if it has been never
                # executed before or already executed in the past.
                try:
                    data['_next_fire_time'] = croniter.croniter(data['cron'], now).get_next(datetime.datetime)
                    data['_next_scheduled_fire_time'] = croniter.croniter(data['cron'], now).get_next(datetime.datetime)
                except (ValueError, KeyError):
                    data['_error'] = ('Invalid cron string. '
                                      'Ignoring job {0}.'.format(data['name']))
                    log.error(data['_error'])
                    return

                # If next job run is scheduled more than 1 minute ahead and
                # configured loop interval is longer than that, we should
                # shorten it to get our job executed closer to the beginning
                # of desired time.
                interval = (now - data['_next_fire_time']).total_seconds()
                if interval >= 60 and interval < self.loop_interval:
                    self.loop_interval = interval

        def _handle_run_explicit(data, loop_interval):
            '''
            Handle schedule item with run_explicit
            '''
            _run_explicit = []
            for _run_time in data['run_explicit']:
                if isinstance(_run_time, datetime.datetime):
                    _run_explicit.append(_run_time)
                else:
                    _run_explicit.append(datetime.datetime.strptime(_run_time['time'],
                                                                    _run_time['time_fmt']))
            data['run'] = False

            # Copy the list so we can loop through it
            for i in copy.deepcopy(_run_explicit):
                if len(_run_explicit) > 1:
                    if i < now - loop_interval:
                        _run_explicit.remove(i)

            if _run_explicit:
                if _run_explicit[0] <= now < _run_explicit[0] + loop_interval:
                    data['run'] = True
                    data['_next_fire_time'] = _run_explicit[0]

        def _handle_skip_explicit(data, loop_interval):
            '''
            Handle schedule item with skip_explicit
            '''
            data['run'] = False

            _skip_explicit = []
            for _skip_time in data['skip_explicit']:
                if isinstance(_skip_time, datetime.datetime):
                    _skip_explicit.append(_skip_time)
                else:
                    _skip_explicit.append(datetime.datetime.strptime(_skip_time['time'],
                                                                     _skip_time['time_fmt']))

            # Copy the list so we can loop through it
            for i in copy.deepcopy(_skip_explicit):
                if i < now - loop_interval:
                    _skip_explicit.remove(i)

            if _skip_explicit:
                if _skip_explicit[0] <= now <= (_skip_explicit[0] + loop_interval):
                    if self.skip_function:
                        data['run'] = True
                        data['func'] = self.skip_function
                    else:
                        data['_skip_reason'] = 'skip_explicit'
                        data['_skipped_time'] = now
                        data['_skipped'] = True
                        data['run'] = False
            else:
                data['run'] = True

        def _handle_skip_during_range(data, loop_interval):
            '''
            Handle schedule item with skip_explicit
            '''
            if not _RANGE_SUPPORTED:
                data['_error'] = ('Missing python-dateutil. '
                                  'Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])
                return

            if not isinstance(data['skip_during_range'], dict):
                data['_error'] = ('schedule.handle_func: Invalid, range '
                                  'must be specified as a dictionary. '
                                  'Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])
                return

            start = data['skip_during_range']['start']
            end = data['skip_during_range']['end']
            if not isinstance(start, datetime.datetime):
                try:
                    start = dateutil_parser.parse(start)
                except ValueError:
                    data['_error'] = ('Invalid date string for start in '
                                      'skip_during_range. Ignoring '
                                      'job {0}.'.format(data['name']))
                    log.error(data['_error'])
                    return

            if not isinstance(end, datetime.datetime):
                try:
                    end = dateutil_parser.parse(end)
                except ValueError:
                    data['_error'] = ('Invalid date string for end in '
                                      'skip_during_range. Ignoring '
                                      'job {0}.'.format(data['name']))
                    log.error(data['_error'])
                    return

            # Check to see if we should run the job immediately
            # after the skip_during_range is over
            if 'run_after_skip_range' in data and \
               data['run_after_skip_range']:
                if 'run_explicit' not in data:
                    data['run_explicit'] = []
                # Add a run_explicit for immediately after the
                # skip_during_range ends
                _run_immediate = (end + loop_interval).strftime('%Y-%m-%dT%H:%M:%S')
                if _run_immediate not in data['run_explicit']:
                    data['run_explicit'].append({'time': _run_immediate,
                                                 'time_fmt': '%Y-%m-%dT%H:%M:%S'})

            if end > start:
                if start <= now <= end:
                    if self.skip_function:
                        data['run'] = True
                        data['func'] = self.skip_function
                    else:
                        data['_skip_reason'] = 'in_skip_range'
                        data['_skipped_time'] = now
                        data['_skipped'] = True
                        data['run'] = False
                else:
                    data['run'] = True
            else:
                data['_error'] = ('schedule.handle_func: Invalid '
                                  'range, end must be larger than '
                                  'start. Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])

        def _handle_range(data):
            '''
            Handle schedule item with skip_explicit
            '''
            if not _RANGE_SUPPORTED:
                data['_error'] = ('Missing python-dateutil. '
                                  'Ignoring job {0}'.format(data['name']))
                log.error(data['_error'])
                return

            if not isinstance(data['range'], dict):
                data['_error'] = ('schedule.handle_func: Invalid, range '
                                  'must be specified as a dictionary.'
                                  'Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])
                return

            start = data['range']['start']
            end = data['range']['end']
            if not isinstance(start, datetime.datetime):
                try:
                    start = dateutil_parser.parse(start)
                except ValueError:
                    data['_error'] = ('Invalid date string for start. '
                                      'Ignoring job {0}.'.format(data['name']))
                    log.error(data['_error'])
                    return

            if not isinstance(end, datetime.datetime):
                try:
                    end = dateutil_parser.parse(end)
                except ValueError:
                    data['_error'] = ('Invalid date string for end.'
                                      ' Ignoring job {0}.'.format(data['name']))
                    log.error(data['_error'])
                    return

            if end > start:
                if 'invert' in data['range'] and data['range']['invert']:
                    if now <= start or now >= end:
                        data['run'] = True
                    else:
                        data['_skip_reason'] = 'in_skip_range'
                        data['run'] = False
                else:
                    if start <= now <= end:
                        data['run'] = True
                    else:
                        if self.skip_function:
                            data['run'] = True
                            data['func'] = self.skip_function
                        else:
                            data['_skip_reason'] = 'not_in_range'
                            data['run'] = False
            else:
                data['_error'] = ('schedule.handle_func: Invalid '
                                  'range, end must be larger '
                                  'than start. Ignoring job {0}.'.format(data['name']))
                log.error(data['_error'])

        def _handle_after(data):
            '''
            Handle schedule item with after
            '''
            if not _WHEN_SUPPORTED:
                data['_error'] = ('Missing python-dateutil. '
                                  'Ignoring job {0}'.format(data['name']))
                log.error(data['_error'])
                return

            after = data['after']
            if not isinstance(after, datetime.datetime):
                after = dateutil_parser.parse(after)

            if after >= now:
                log.debug(
                    'After time has not passed skipping job: %s.',
                    data['name']
                )
                data['_skip_reason'] = 'after_not_passed'
                data['_skipped_time'] = now
                data['_skipped'] = True
                data['run'] = False
            else:
                data['run'] = True

        def _handle_until(data):
            '''
            Handle schedule item with until
            '''
            if not _WHEN_SUPPORTED:
                data['_error'] = ('Missing python-dateutil. '
                                  'Ignoring job {0}'.format(data['name']))
                log.error(data['_error'])
                return

            until = data['until']
            if not isinstance(until, datetime.datetime):
                until = dateutil_parser.parse(until)

            if until <= now:
                log.debug(
                    'Until time has passed skipping job: %s.',
                    data['name']
                )
                data['_skip_reason'] = 'until_passed'
                data['_skipped_time'] = now
                data['_skipped'] = True
                data['run'] = False
            else:
                data['run'] = True

        def _chop_ms(dt):
            '''
            Remove the microseconds from a datetime object
            '''
            return dt - datetime.timedelta(microseconds=dt.microsecond)

        schedule = self._get_schedule()
        if not isinstance(schedule, dict):
            raise ValueError('Schedule must be of type dict.')
        if 'skip_function' in schedule:
            self.skip_function = schedule['skip_function']
        if 'skip_during_range' in schedule:
            self.skip_during_range = schedule['skip_during_range']
        if 'enabled' in schedule:
            self.enabled = schedule['enabled']
        if 'splay' in schedule:
            self.splay = schedule['splay']

        _hidden = ['enabled',
                   'skip_function',
                   'skip_during_range',
                   'splay']
        for job, data in six.iteritems(schedule):

            # Skip anything that is a global setting
            if job in _hidden:
                continue

            # Clear these out between runs
            for item in ['_continue',
                         '_error',
                         '_enabled',
                         '_skipped',
                         '_skip_reason',
                         '_skipped_time']:
                if item in data:
                    del data[item]
            run = False

            if 'name' in data:
                job_name = data['name']
            else:
                job_name = data['name'] = job

            if not isinstance(data, dict):
                log.error(
                    'Scheduled job "%s" should have a dict value, not %s',
                    job_name, type(data)
                )
                continue
            if 'function' in data:
                func = data['function']
            elif 'func' in data:
                func = data['func']
            elif 'fun' in data:
                func = data['fun']
            else:
                func = None
            if not isinstance(func, list):
                func = [func]
            for _func in func:
                if _func not in self.functions:
                    log.info(
                        'Invalid function: %s in scheduled job %s.',
                        _func, job_name
                    )

            if '_next_fire_time' not in data:
                data['_next_fire_time'] = None

            if '_splay' not in data:
                data['_splay'] = None

            if 'run_on_start' in data and \
                    data['run_on_start'] and \
                    '_run_on_start' not in data:
                data['_run_on_start'] = True

            if not now:
                now = datetime.datetime.now()

            # Used for quick lookups when detecting invalid option
            # combinations.
            schedule_keys = set(data.keys())

            time_elements = ('seconds', 'minutes', 'hours', 'days')
            scheduling_elements = ('when', 'cron', 'once')

            invalid_sched_combos = [
                set(i) for i in itertools.combinations(scheduling_elements, 2)
            ]

            if any(i <= schedule_keys for i in invalid_sched_combos):
                log.error(
                    'Unable to use "%s" options together. Ignoring.',
                    '", "'.join(scheduling_elements)
                )
                continue

            invalid_time_combos = []
            for item in scheduling_elements:
                all_items = itertools.chain([item], time_elements)
                invalid_time_combos.append(
                    set(itertools.combinations(all_items, 2)))

            if any(set(x) <= schedule_keys for x in invalid_time_combos):
                log.error(
                    'Unable to use "%s" with "%s" options. Ignoring',
                    '", "'.join(time_elements),
                    '", "'.join(scheduling_elements)
                )
                continue

            if 'run_explicit' in data:
                _handle_run_explicit(data, loop_interval)
                run = data['run']

            if True in [True for item in time_elements if item in data]:
                _handle_time_elements(data)
            elif 'once' in data:
                _handle_once(data, loop_interval)
            elif 'when' in data:
                _handle_when(data, loop_interval)
            elif 'cron' in data:
                _handle_cron(data, loop_interval)
            else:
                continue

            # Something told us to continue, so we continue
            if '_continue' in data and data['_continue']:
                continue

            # An error occurred so we bail out
            if '_error' in data and data['_error']:
                continue

            seconds = int((_chop_ms(data['_next_fire_time']) - _chop_ms(now)).total_seconds())

            # If there is no job specific splay available,
            # grab the global which defaults to None.
            if 'splay' not in data:
                data['splay'] = self.splay

            if 'splay' in data and data['splay']:
                # Got "splay" configured, make decision to run a job based on that
                if not data['_splay']:
                    # Try to add "splay" time only if next job fire time is
                    # still in the future. We should trigger job run
                    # immediately otherwise.
                    splay = _splay(data['splay'])
                    if now < data['_next_fire_time'] + datetime.timedelta(seconds=splay):
                        log.debug('schedule.handle_func: Adding splay of '
                                  '%s seconds to next run.', splay)
                        data['_splay'] = data['_next_fire_time'] + datetime.timedelta(seconds=splay)
                        if 'when' in data:
                            data['_run'] = True
                    else:
                        run = True

                if data['_splay']:
                    # The "splay" configuration has been already processed, just use it
                    seconds = (data['_splay'] - now).total_seconds()
                    if 'when' in data:
                        data['_next_fire_time'] = data['_splay']

            if '_seconds' in data:
                if seconds <= 0:
                    run = True
            elif 'when' in data and data['_run']:
                if data['_next_fire_time'] <= now <= (data['_next_fire_time'] + loop_interval):
                    data['_run'] = False
                    run = True
            elif 'cron' in data:
                # Reset next scheduled time because it is in the past now,
                # and we should trigger the job run, then wait for the next one.
                if seconds <= 0:
                    data['_next_fire_time'] = None
                    run = True
            elif 'once' in data:
                if data['_next_fire_time'] <= now <= (data['_next_fire_time'] + loop_interval):
                    run = True
            elif seconds == 0:
                run = True

            if '_run_on_start' in data and data['_run_on_start']:
                run = True
                data['_run_on_start'] = False
            elif run:
                if 'range' in data:
                    _handle_range(data)

                    # An error occurred so we bail out
                    if '_error' in data and data['_error']:
                        continue

                    run = data['run']
                    # Override the functiton if passed back
                    if 'func' in data:
                        func = data['func']

                # If there is no job specific skip_during_range available,
                # grab the global which defaults to None.
                if 'skip_during_range' not in data and self.skip_during_range:
                    data['skip_during_range'] = self.skip_during_range

                if 'skip_during_range' in data and data['skip_during_range']:
                    _handle_skip_during_range(data, loop_interval)

                    # An error occurred so we bail out
                    if '_error' in data and data['_error']:
                        continue

                    run = data['run']
                    # Override the functiton if passed back
                    if 'func' in data:
                        func = data['func']

                if 'skip_explicit' in data:
                    _handle_skip_explicit(data, loop_interval)

                    # An error occurred so we bail out
                    if '_error' in data and data['_error']:
                        continue

                    run = data['run']
                    # Override the functiton if passed back
                    if 'func' in data:
                        func = data['func']

                if 'until' in data:
                    _handle_until(data)

                    # An error occurred so we bail out
                    if '_error' in data and data['_error']:
                        continue

                    run = data['run']

                if 'after' in data:
                    _handle_after(data)

                    # An error occurred so we bail out
                    if '_error' in data and data['_error']:
                        continue

                    run = data['run']

                # If args is a list and less than the number of functions
                # run is set to False.
                if 'args' in data and isinstance(data['args'], list):
                    if len(data['args']) < len(func):
                        data['_error'] = ('Number of arguments is less than '
                                          'the number of functions. Ignoring job.')
                        log.error(data['_error'])
                        run = False

            # If the job item has continue, then we set run to False
            # so the job does not run but we still get the important
            # information calculated, eg. _next_fire_time
            if '_continue' in data and data['_continue']:
                run = False

            # If there is no job specific enabled available,
            # grab the global which defaults to True.
            if 'enabled' not in data:
                data['enabled'] = self.enabled

            # If globally disabled, disable the job
            if not self.enabled:
                data['enabled'] = self.enabled
                data['_skip_reason'] = 'disabled'
                data['_skipped_time'] = now
                data['_skipped'] = True
                run = False

            # Job is disabled, set run to False
            if 'enabled' in data and not data['enabled']:
                data['_enabled'] = False
                data['_skip_reason'] = 'disabled'
                data['_skipped_time'] = now
                data['_skipped'] = True
                run = False

            miss_msg = ''
            if seconds < 0:
                miss_msg = ' (runtime missed ' \
                           'by {0} seconds)'.format(abs(seconds))

            try:
                if run:
                    # Job is disabled, continue
                    if 'enabled' in data and not data['enabled']:
                        log.debug('Job: %s is disabled', job_name)
                        data['_skip_reason'] = 'disabled'
                        data['_skipped_time'] = now
                        data['_skipped'] = True
                        continue

                    if 'jid_include' not in data or data['jid_include']:
                        data['jid_include'] = True
                        log.debug('schedule: Job %s was scheduled with jid_include, '
                                  'adding to cache (jid_include defaults to True)',
                                  job_name)
                        if 'maxrunning' in data:
                            log.debug('schedule: Job %s was scheduled with a max '
                                      'number of %s', job_name, data['maxrunning'])
                        else:
                            log.info('schedule: maxrunning parameter was not specified for '
                                     'job %s, defaulting to 1.', job_name)
                            data['maxrunning'] = 1

                    if not self.standalone:
                        data['run'] = run
                        data = self._check_max_running(func,
                                                       data,
                                                       self.opts,
                                                       now)
                        run = data['run']

                # Check run again, just in case _check_max_running
                # set run to False
                if run:
                    log.info('Running scheduled job: %s%s', job_name, miss_msg)
                    self._run_job(func, data)

            finally:
                # Only set _last_run if the job ran
                if run:
                    data['_last_run'] = now
                    data['_splay'] = None
                if '_seconds' in data:
                    if self.standalone:
                        data['_next_fire_time'] = now + datetime.timedelta(seconds=data['_seconds'])
                    elif '_skipped' in data and data['_skipped']:
                        data['_next_fire_time'] = now + datetime.timedelta(seconds=data['_seconds'])
                    elif run:
                        data['_next_fire_time'] = now + datetime.timedelta(seconds=data['_seconds'])