def get_timeuntil(self, obj): if obj.force_run: next_run = dates.localtime(dates.now()) time_until = _("forced") else: next_run = dates.localtime(obj.next_run) time_until = obj.get_timeuntil() value = capfirst(dates.local_dateformat(next_run)) return "%s<br /><span class='mini'>(%s)</span>" % (value, time_until)
def last_run_with_link(self, obj): value = capfirst(dates.local_dateformat(dates.localtime(obj.last_run))) try: log_id = obj.log_set.latest('run_date').id try: # Old way url = reverse('chronograph_log_change', args=(log_id,)) except NoReverseMatch: # New way url = reverse('admin:chronograph_log_change', args=(log_id,)) return '<a href="%s">%s</a>' % (url, value) except: return value
def last_run_with_link(self, obj): value = capfirst(dates.local_dateformat(dates.localtime(obj.last_run))) try: log_id = obj.log_set.latest('run_date').id try: # Old way url = reverse('chronograph_log_change', args=(log_id, )) except NoReverseMatch: # New way url = reverse('admin:chronograph_log_change', args=(log_id, )) return '<a href="%s">%s</a>' % (url, value) except: return value
class Job(models.Model): """ A recurring ``django-admin`` command to be run. """ name = models.CharField(_("name"), max_length=200) frequency = models.CharField(_("frequency"), choices=freqs, max_length=10) params = models.TextField(_("params"), null=True, blank=True, help_text=_( 'Comma-separated list of ' '<a href="http://labix.org/python-dateutil" ' 'target="_blank">rrule parameters</a>. ' 'e.g: interval:15')) command = models.CharField(_("command"), max_length=200, blank=True, help_text=_("A valid django-admin command to " "run.")) args = models.CharField( _("args"), max_length=200, blank=True, help_text=_("Space separated list; e.g: arg1 option1=True")) disabled = models.BooleanField(default=False, help_text=_('If checked this ' 'job will never ' 'run.')) next_run = models.DateTimeField(_("next run"), blank=True, null=True, help_text=_( "If you don't set this it will" " be determined automatically")) last_run = models.DateTimeField(_("last run"), editable=False, blank=True, null=True) is_running = models.BooleanField(default=False, editable=False) last_run_successful = models.BooleanField(default=True, blank=False, null=False, editable=False) subscribers = models.ManyToManyField(User, blank=True, limit_choices_to={'is_staff': True}) lock_file = models.CharField(max_length=255, blank=True, editable=False) force_run = models.BooleanField(default=False) objects = JobManager() class Meta: ordering = ( 'disabled', 'next_run', ) def __unicode__(self): if self.disabled: return _(u"%(name)s - disabled") % {'name': self.name} return u"%s - %s" % (self.name, self.timeuntil) def save(self, *args, **kwargs): if not self.disabled: if self.pk: j = Job.objects.get(pk=self.pk) else: j = self if not self.next_run or j.params != self.params: logger.debug("Updating 'next_run") next_run = self.next_run if not next_run: next_run = dates.now() next_run = dates.make_naive(next_run) self.next_run = dates.make_aware(self.rrule.after(next_run)) else: self.next_run = None super(Job, self).save(*args, **kwargs) def get_timeuntil(self): """ Returns a string representing the time until the next time this Job will be run (actually, the "string" returned is really an instance of ``ugettext_lazy``). >>> from chronograph.compatibility.dates import now >>> job = Job(next_run=now()) >>> job.get_timeuntil().translate('en') u'due' """ if self.disabled: return _('never (disabled)') delta = self.next_run - dates.now() if delta.days < 0: # The job is past due and should be run as soon as possible if self.check_is_running(): return _('running') return _('due') elif delta.seconds < 60: # Adapted from django.utils.timesince count = lambda n: ungettext('second', 'seconds', n) return ugettext('%(number)d %(type)s') % { 'number': delta.seconds, 'type': count(delta.seconds) } return timeuntil(self.next_run) get_timeuntil.short_description = _('time until next run') timeuntil = property(get_timeuntil) def get_rrule(self): """ Returns the rrule objects for this ``Job``. Can also be accessed via the ``rrule`` property of the ``Job``. # Every minute >>> last_run = datetime(2011, 8, 4, 7, 19) >>> job = Job(frequency="MINUTELY", params="interval:1", last_run=last_run) >>> print job.get_rrule().after(last_run) 2011-08-04 07:20:00 # Every 2 hours >>> job = Job(frequency="HOURLY", params="interval:2", last_run=last_run) >>> print job.get_rrule().after(last_run) 2011-08-04 09:19:00 """ frequency = eval('rrule.%s' % self.frequency) return rrule.rrule(frequency, dtstart=self.last_run, **self.get_params()) rrule = property(get_rrule) def param_to_int(self, param_value): """ Converts a valid rrule parameter to an integer if it is not already one, else raises a ``ValueError``. The following are equivalent: >>> job = Job(params = "byweekday:1,2,4,5") >>> job.get_params() {'byweekday': [1, 2, 4, 5]} >>> job = Job(params = "byweekday:TU,WE,FR,SA") >>> job.get_params() {'byweekday': [1, 2, 4, 5]} """ if param_value in RRULE_WEEKDAY_DICT: return RRULE_WEEKDAY_DICT[param_value] try: val = int(param_value) except ValueError: raise ValueError('rrule parameter should be integer or weekday ' 'constant (e.g. MO, TU, etc.). ' 'Error on: %s' % param_value) else: return val def get_params(self): """ Converts a string of parameters into a dict. >>> job = Job(params = "count:1;bysecond:1;byminute:1,2,4,5") >>> job.get_params() {'count': 1, 'byminute': [1, 2, 4, 5], 'bysecond': 1} """ if self.params is None: return {} params = self.params.split(';') param_dict = [] for param in params: if param.strip() == "": continue # skip blanks param = param.split(':') if len(param) == 2: param = (str(param[0]).strip(), [ self.param_to_int(p.strip()) for p in param[1].split(',') ]) if len(param[1]) == 1: param = (param[0], param[1][0]) param_dict.append(param) return dict(param_dict) def get_args(self): """ Processes the args and returns a tuple or (args, options) for passing to ``call_command``. >>> job = Job(args="arg1 arg2 kwarg1='some value'") >>> job.get_args() (['arg1', 'arg2', "value'"], {'kwarg1': "'some"}) """ args = [] options = {} for arg in self.args.split(): if arg.find('=') > -1: key, value = arg.split('=') options[smart_str(key)] = smart_str(value) else: args.append(arg) return (args, options) def is_due(self): """ >>> from chronograph.compatibility.dates import now >>> job = Job(next_run=now()) >>> job.is_due() True >>> job = Job(next_run=now()+timedelta(seconds=60)) >>> job.is_due() False >>> job.force_run = True >>> job.is_due() True >>> job = Job(next_run=now(), disabled=True) >>> job.is_due() False """ reqs = (self.next_run <= dates.now() and self.disabled == False and self.check_is_running() == False) return (reqs or self.force_run) def run(self): """ Runs this ``Job``. A ``Log`` will be created if there is any output from either stdout or stderr. Returns ``True`` if the ``Job`` ran, ``False`` otherwise. """ if not self.disabled: if not self.check_is_running() and self.is_due(): call_command('run_job', str(self.pk)) return True return False def handle_run(self): """ This method implements the code to actually run a ``Job``. This is meant to be run, primarily, by the `run_job` management command as a subprocess, which can be invoked by calling this ``Job``\'s ``run`` method. """ args, options = self.get_args() stdout = StringIO() stderr = StringIO() # Redirect output so that we can log it if there is any ostdout = sys.stdout ostderr = sys.stderr sys.stdout = stdout sys.stderr = stderr stdout_str, stderr_str = "", "" heartbeat = JobHeartbeatThread() run_date = dates.now() self.is_running = True self.lock_file = heartbeat.lock_file.name self.save() heartbeat.start() try: logger.debug("Calling command '%s'" % self.command) call_command(self.command, *args, **options) logger.debug("Command '%s' completed" % self.command) self.last_run_successful = True except Exception, e: # The command failed to run; log the exception t = loader.get_template('chronograph/error_message.txt') c = Context({ 'exception': unicode(e), 'traceback': ['\n'.join(traceback.format_exception(*sys.exc_info()))] }) stderr_str += t.render(c) self.last_run_successful = False # Stop the heartbeat logger.debug("Stopping heartbeat") heartbeat.stop() heartbeat.join() duration = dates.total_seconds(dates.now() - run_date) self.is_running = False self.lock_file = "" # Only care about minute-level resolution self.last_run = dates.localtime( dates.make_aware( datetime(run_date.year, run_date.month, run_date.day, run_date.hour, run_date.minute))) # If this was a forced run, then don't update the # next_run date if self.force_run: logger.debug("Resetting 'force_run'") self.force_run = False else: logger.debug("Determining 'next_run'") while self.next_run < dates.now(): self.next_run = dates.make_aware( self.rrule.after(self.next_run)) logger.debug("'next_run = ' %s" % self.next_run) self.save() # If we got any output, save it to the log stdout_str += stdout.getvalue() stderr_str += stderr.getvalue() if stderr_str: # If anything was printed to stderr, consider the run # unsuccessful self.last_run_successful = False if stdout_str or stderr_str: log = Log.objects.create(job=self, run_date=run_date, stdout=stdout_str, stderr=stderr_str, success=self.last_run_successful, duration=duration) # Redirect output back to default sys.stdout = ostdout sys.stderr = ostderr