diff --git a/slapos/recipe/dcron.py b/slapos/recipe/dcron.py index 3b15ce35e4876e1527ac2a5ea6a4773268fdfa69..de4db32cd7c1321ca1b1ce6e131945def846280c 100644 --- a/slapos/recipe/dcron.py +++ b/slapos/recipe/dcron.py @@ -26,7 +26,11 @@ ############################################################################## import os -from slapos.recipe.librecipe import GenericBaseRecipe +if __name__ == '__main__': # Hack to easily run test below. + GenericBaseRecipe = object +else: + from slapos.recipe.librecipe import GenericBaseRecipe + from zc.buildout import UserError class Recipe(GenericBaseRecipe): @@ -51,18 +55,111 @@ class Recipe(GenericBaseRecipe): return [script] - class Part(GenericBaseRecipe): def install(self): + try: + periodicity = self.options['frequency'] + except KeyError: + periodicity = self.options['time'] + try: + periodicity = systemd_to_cron(periodicity) + except Exception: + raise UserError("Invalid systemd calendar spec %r" % periodicity) cron_d = self.options['cron-entries'] name = self.options['name'] filename = os.path.join(cron_d, name) with open(filename, 'w') as part: - part.write('%(frequency)s %(command)s\n' % { - 'frequency': self.options['frequency'], - 'command': self.options['command'], - }) + part.write('%s %s\n' % (periodicity, self.options['command'])) return [filename] + + +day_of_week_dict = dict((name, dow) for dow, name in enumerate( + "sunday monday tuesday wednesday thursday friday saturday".split()) + for name in (name, name[:3])) + +def systemd_to_cron(spec): + """Convert from systemd.time(7) calendar spec to crontab spec""" + if spec in ("hourly", "daily", "monthly", "weekly"): + return '@' + spec + if not spec.strip(): + raise ValueError + spec = spec.split(' ') + try: + dow = ','.join(sorted('-'.join(str(day_of_week_dict[x.lower()]) + for x in x.split('-', 1)) + for x in spec[0].split(',') + if x)) + del spec[0] + except KeyError: + dow = '*' + day = spec.pop(0) if spec else '*-*' + if spec: + time, = spec + elif ':' in day: + time = day + day = '*-*' + else: + time = '0:0' + day = day.split('-') + time = time.split(':') + if (# years not supported + len(day) > 2 and day.pop(0) != '*' or + # some crons ignore day of month if day of week is given, and dcron + # treats day of month in a way that is not compatible with systemd + dow != '*' != day[1] or + # seconds not supported + len(time) > 2 and int(time.pop())): + raise ValueError + month, day = day + hour, minute = time + spec = minute, hour, day, month, dow + for x, (y, z) in zip(spec, ((0, 60), (0, 24), (1, 31), (1, 12))): + if x != '*': + for x in x.split(','): + x = map(int, x.split('/', 1)) + x[0] -= y + if x[0] < 0 or len(x) > 1 and x[0] >= x[1] or z <= sum(x): + raise ValueError + return ' '.join(spec) + +def test(self): + def _(systemd, cron): + self.assertEqual(systemd_to_cron(systemd), cron) + _("Sat,Mon-Thu,Sun", "0 0 * * 0,1-4,6") + _("mon,sun *-* 2,1:23", "23 2,1 * * 0,1") + _("Wed, 17:48", "48 17 * * 3") + _("Wed-Sat,Tue 10-* 1:2", "2 1 * 10 2,3-6") + _("*-*-7 0:0:0", "0 0 7 * *") + _("10-15", "0 0 15 10 *") + _("monday *-12-* 17:00", "00 17 * 12 1") + _("12,14,13,12:20,10,30", "20,10,30 12,14,13,12 * * *") # TODO: sort + _("*-1/2-1,3 *:30", "30 * 1,3 1/2 *") + _("03-05 08:05", "05 08 05 03 *") + _("08:05:00", "05 08 * * *") + _("05:40", "40 05 * * *") + _("Sat,Sun 12-* 08:05", "05 08 * 12 0,6") + _("Sat,Sun 08:05", "05 08 * * 0,6") + + def _(systemd): + self.assertRaises(Exception, systemd_to_cron, systemd) + _("test") + _("") + _("7") + _("121212:1:2") + + _("Wed *-1") + _("08:05:40") + _("2003-03-05") + + _("0-1"); _("13-1"); _("6/4-1"); _("5/8-1") + _("1-0"); _("1-32"); _("1-4/3"); _("1-14/18") + _("24:0");_("9/9:0"); _("8/16:0") + _("0:60"); _("0:22/22"); _("0:15/45") + +if __name__ == '__main__': + import unittest + unittest.TextTestRunner().run(type('', (unittest.TestCase,), { + 'runTest': test})())