launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00468
[Merge] lp:~stub/launchpad/cronscripts into lp:launchpad/devel
Stuart Bishop has proposed merging lp:~stub/launchpad/cronscripts into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Steps towards Bug #607391.
Adds a config file controlling cronscripts. This config file allows them to be selectively or in bulk disabled from running.
--
https://code.launchpad.net/~stub/launchpad/cronscripts/+merge/31934
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stub/launchpad/cronscripts into lp:launchpad/devel.
=== modified file 'configs/testrunner/launchpad-lazr.conf'
--- configs/testrunner/launchpad-lazr.conf 2010-07-16 20:55:29 +0000
+++ configs/testrunner/launchpad-lazr.conf 2010-08-06 09:11:12 +0000
@@ -7,6 +7,7 @@
[canonical]
chunkydiff: False
+cron_control_file: lib/lp/services/scripts/tests/cronscripts.ini
[archivepublisher]
base_url: http://ftpmaster.internal/
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2010-08-05 06:27:00 +0000
+++ lib/canonical/config/schema-lazr.conf 2010-08-06 09:11:12 +0000
@@ -209,6 +209,10 @@
# datatype: string
admin_address: system-error@xxxxxxxxxxxxx
+# By default, relative to the root.
+# datatype: filename
+cron_control_file: cronscripts.ini
+
[checkwatches]
# The database user to run this process as.
@@ -796,7 +800,7 @@
# If true, only the source package names are imported into
# Launchpad
-# datatype: boolean"
+# datatype: boolean
sourcepackagenames_only: false
=== modified file 'lib/lp/services/scripts/base.py'
--- lib/lp/services/scripts/base.py 2010-04-27 06:15:37 +0000
+++ lib/lp/services/scripts/base.py 2010-08-06 09:11:12 +0000
@@ -9,17 +9,19 @@
'SilentLaunchpadScriptFailure'
]
+from ConfigParser import SafeConfigParser
from cProfile import Profile
import datetime
import logging
from optparse import OptionParser
-import os
+import os.path
import sys
from contrib.glock import GlobalLock, LockAlreadyAcquired
import pytz
from zope.component import getUtility
+from canonical.config import config
from canonical.database.sqlbase import ISOLATION_LEVEL_DEFAULT
from canonical.launchpad import scripts
from canonical.launchpad.interfaces import IScriptActivitySet
@@ -185,8 +187,8 @@
def setup_lock(self):
"""Create lockfile.
- Note that this will create a lockfile even if you don't actually use it.
- GlobalLock.__del__ is meant to clean it up though.
+ Note that this will create a lockfile even if you don't actually
+ use it. GlobalLock.__del__ is meant to clean it up though.
"""
self.lock = GlobalLock(self.lockfilepath, logger=self.logger)
@@ -290,6 +292,22 @@
class LaunchpadCronScript(LaunchpadScript):
"""Logs successful script runs in the database."""
+ def __init__(self, name=None, dbuser=None, test_args=None):
+ """Initialize, and sys.exit() if the cronscript is disabled.
+
+ Rather than hand editing crontab files, cronscripts can be
+ enabled and disabled using a config file.
+
+ The control file location is specified by
+ config.canonical.cron_control_file.
+ """
+ super(LaunchpadCronScript, self).__init__(name, dbuser, test_args)
+
+ enabled = cronscript_enabled(
+ config.canonical.cron_control_file, self.name, self.logger)
+ if not enabled:
+ sys.exit(0)
+
def record_activity(self, date_started, date_completed):
"""Record the successful completion of the script."""
self.txn.begin()
@@ -299,3 +317,45 @@
date_started=date_started,
date_completed=date_completed)
self.txn.commit()
+
+
+def cronscript_enabled(control_path, name, log):
+ """Return True if the cronscript is enabled."""
+ if not os.path.isabs(control_path):
+ control_path = os.path.abspath(
+ os.path.join(config.root, control_path))
+
+ cron_config = SafeConfigParser({'enabled': str(True)})
+
+ if not os.path.exists(control_path):
+ # No control file exists. Everything enabled by default.
+ log.debug("Cronscript control file not found at %s", control_path)
+ else:
+ log.debug("Cronscript control file found at %s", control_path)
+
+ # Try reading the config file. If it fails, we log the
+ # traceback and continue on using the defaults.
+ try:
+ cron_config.read(control_path)
+ except:
+ log.exception("Error parsing %s", control_path)
+
+ if cron_config.has_option(name, 'enabled'):
+ section = name
+ else:
+ section = 'DEFAULT'
+
+ try:
+ enabled = cron_config.getboolean(section, 'enabled')
+ except:
+ log.exception(
+ "Failed to load value from %s section of %s",
+ section, control_path)
+ enabled = True
+
+ if enabled:
+ log.debug("Enabled by %s section", section)
+ else:
+ log.info("Disabled by %s section", section)
+
+ return enabled
=== added file 'lib/lp/services/scripts/tests/cronscripts.ini'
--- lib/lp/services/scripts/tests/cronscripts.ini 1970-01-01 00:00:00 +0000
+++ lib/lp/services/scripts/tests/cronscripts.ini 2010-08-06 09:11:12 +0000
@@ -0,0 +1,2 @@
+[example-cronscript-disabled]
+enabled: False
=== added file 'lib/lp/services/scripts/tests/example-cronscript.py'
--- lib/lp/services/scripts/tests/example-cronscript.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/scripts/tests/example-cronscript.py 2010-08-06 09:11:12 +0000
@@ -0,0 +1,30 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""An example cronscript. If it runs, it returns 42 as its return code."""
+
+__metaclass__ = type
+__all__ = []
+
+import sys
+
+from lp.services.scripts.base import (
+ LaunchpadCronScript, SilentLaunchpadScriptFailure)
+
+class Script(LaunchpadCronScript):
+ def main(self):
+ if self.name == 'example-cronscript-enabled':
+ raise SilentLaunchpadScriptFailure(42)
+ else:
+ # Raise a non-standard error code, as if the
+ # script was invoked as disabled the main()
+ # method should never be invoked.
+ raise SilentLaunchpadScriptFailure(999)
+
+if __name__ == '__main__':
+ if sys.argv[-1] == 'enabled':
+ name = 'example-cronscript-enabled'
+ else:
+ name = 'example-cronscript-disabled'
+ script = Script(name)
+ script.lock_and_run()
=== added file 'lib/lp/services/scripts/tests/test_cronscript_enabled.py'
--- lib/lp/services/scripts/tests/test_cronscript_enabled.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/scripts/tests/test_cronscript_enabled.py 2010-08-06 09:11:12 +0000
@@ -0,0 +1,144 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test the cronscript_enabled function in scripts/base.py."""
+
+__metaclass__ = type
+
+from cStringIO import StringIO
+from logging import DEBUG
+import os.path
+import subprocess
+import sys
+from tempfile import NamedTemporaryFile
+from textwrap import dedent
+import unittest
+
+from lp.services.scripts.base import cronscript_enabled
+from lp.testing import TestCase
+from lp.testing.logger import MockLogger
+
+
+class TestCronscriptEnabled(TestCase):
+ def setUp(self):
+ super(TestCronscriptEnabled, self).setUp()
+ self.log_output = StringIO()
+ self.log = MockLogger(self.log_output)
+ self.log.setLevel(DEBUG)
+
+ def makeConfig(self, body):
+ tempfile = NamedTemporaryFile(suffix='.ini')
+ tempfile.write(body)
+ tempfile.flush()
+ # Ensure a reference is kept until the test is over.
+ # tempfile will then clean itself up.
+ self.addCleanup(lambda x: None, tempfile)
+ return tempfile.name
+
+ def test_noconfig(self):
+ enabled = cronscript_enabled('/idontexist.ini', 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_emptyconfig(self):
+ config = self.makeConfig('')
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_default_true(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ enabled: True
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_default_false(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ enabled: False
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(False, enabled)
+
+ def test_specific_true(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ enabled: False
+ [foo]
+ enabled: True
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_specific_false(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ enabled: True
+ [foo]
+ enabled: False
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(False, enabled)
+
+ def test_broken_true(self):
+ config = self.makeConfig(dedent("""\
+ # This file is unparsable
+ [DEFAULT
+ enabled: False
+ [foo
+ enabled: False
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_invalid_boolean_true(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ enabled: whoops
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_specific_missing_fallsback(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ enabled: False
+ [foo]
+ # There is a typo in the next line.
+ enobled: True
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(False, enabled)
+
+ def test_default_missing_fallsback(self):
+ config = self.makeConfig(dedent("""\
+ [DEFAULT]
+ # There is a typo in the next line. Fallsback to hardcoded
+ # default.
+ enobled: False
+ [foo]
+ # There is a typo in the next line.
+ enobled: False
+ """))
+ enabled = cronscript_enabled(config, 'foo', self.log)
+ self.assertIs(True, enabled)
+
+ def test_enabled_cronscript(self):
+ cmd = [
+ sys.executable,
+ os.path.join(os.path.dirname(__file__), 'example-cronscript.py'),
+ '-qqqqq', 'enabled',
+ ]
+ self.assertEqual(42, subprocess.call(cmd))
+
+ def test_disabled_cronscript(self):
+ cmd = [
+ sys.executable,
+ os.path.join(os.path.dirname(__file__), 'example-cronscript.py'),
+ '-qqqqq', 'disabled',
+ ]
+ self.assertEqual(0, subprocess.call(cmd))
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
=== modified file 'lib/lp/testing/tests/test_standard_test_template.py'
--- lib/lp/testing/tests/test_standard_test_template.py 2010-07-17 21:13:21 +0000
+++ lib/lp/testing/tests/test_standard_test_template.py 2010-08-06 09:11:12 +0000
@@ -5,6 +5,8 @@
__metaclass__ = type
+import unittest
+
from canonical.testing import DatabaseFunctionalLayer
from lp.testing import TestCase
@@ -22,3 +24,7 @@
# XXX: Assertions take expected value first, actual value second.
self.assertEqual(4, 2 + 2)
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)