# HG changeset patch # User Thierry Florac # Date 1426071179 -3600 # Node ID 48483b0b26fab8120327bb16b1e6da03176ec5e1 First commit diff -r 000000000000 -r 48483b0b26fa .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,19 @@ + +syntax: regexp +^develop-eggs$ +syntax: regexp +^parts$ +syntax: regexp +^bin$ +syntax: regexp +^\.installed\.cfg$ +syntax: regexp +^\.settings$ +syntax: regexp +^build$ +syntax: regexp +^dist$ +syntax: regexp +^\.idea$ +syntax: regexp +.*\.pyc$ diff -r 000000000000 -r 48483b0b26fa LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,42 @@ +Zope Public License (ZPL) Version 2.1 +===================================== + +A copyright notice accompanies this license document that identifies +the copyright holders. + +This license has been certified as open source. It has also been designated +as GPL compatible by the Free Software Foundation (FSF). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions in source code must retain the accompanying copyright + notice, this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the accompanying copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Names of the copyright holders must not be used to endorse or promote + products derived from this software without prior written permission + from the copyright holders. + 4. The right to distribute this software or to use it for any purpose does + not give you the right to use Servicemarks (sm) or Trademarks (tm) of the + copyright holders. Use of them is covered by separate agreement with the + copyright holders. + 5. If any files are modified, you must cause the modified files to carry + prominent notices stating that you changed the files and the date of any + change. + + +Disclaimer +========== + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff -r 000000000000 -r 48483b0b26fa MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,5 @@ +include *.txt +recursive-include docs * +recursive-include src * +global-exclude *.pyc +global-exclude *.*~ diff -r 000000000000 -r 48483b0b26fa bootstrap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bootstrap.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,178 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os +import shutil +import sys +import tempfile + +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --find-links to point to local resources, you can keep +this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", help="use a specific zc.buildout version") + +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", "--config-file", + help=("Specify the path to the buildout configuration " + "file to be used.")) +parser.add_option("-f", "--find-links", + help=("Specify a URL to search for buildout releases")) +parser.add_option("--allow-site-packages", + action="store_true", default=False, + help=("Let bootstrap.py use existing site packages")) + + +options, args = parser.parse_args() + +###################################################################### +# load/install setuptools + +try: + if options.allow_site_packages: + import setuptools + import pkg_resources + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +ez = {} +exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) + +if not options.allow_site_packages: + # ez_setup imports site, which adds site packages + # this will remove them from the path to ensure that incompatible versions + # of setuptools are not in the path + import site + # inside a virtualenv, there is no 'getsitepackages'. + # We can't remove these reliably + if hasattr(site, 'getsitepackages'): + for sitepackage_path in site.getsitepackages(): + sys.path[:] = [x for x in sys.path if sitepackage_path not in x] + +setup_args = dict(to_dir=tmpeggs, download_delay=0) +ez['use_setuptools'](**setup_args) +import setuptools +import pkg_resources + +# This does not (always?) update the default working set. We will +# do it. +for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +###################################################################### +# Install buildout + +ws = pkg_resources.working_set + +cmd = [sys.executable, '-c', + 'from setuptools.command.easy_install import main; main()', + '-mZqNxd', tmpeggs] + +find_links = os.environ.get( + 'bootstrap-testing-find-links', + options.find_links or + ('http://downloads.buildout.org/' + if options.accept_buildout_test_releases else None) + ) +if find_links: + cmd.extend(['-f', find_links]) + +setuptools_path = ws.find( + pkg_resources.Requirement.parse('setuptools')).location + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setuptools_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +import subprocess +if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: + raise Exception( + "Failed to execute command:\n%s" % repr(cmd)[1:-1]) + +###################################################################### +# Import and run buildout + +ws.add_entry(tmpeggs) +ws.require(requirement) +import zc.buildout.buildout + +if not [a for a in args if '=' not in a]: + args.append('bootstrap') + +# if -c was provided, we push it back into args for buildout' main function +if options.config_file is not None: + args[0:0] = ['-c', options.config_file] + +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff -r 000000000000 -r 48483b0b26fa buildout.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/buildout.cfg Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,70 @@ +[buildout] +eggs-directory = /var/local/env/pyams/eggs + +socket-timeout = 3 +show-picked-versions = true +newest = false + +allow-hosts = + bitbucket.org + *.python.org + *.sourceforge.net + github.com + +#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg +versions = versions +newest = false +#allow-picked-versions = false + +src = src +develop = . + ../pyams_form + ../pyams_mail + ../pyams_pagelet + ../pyams_skin + ../pyams_template + ../pyams_utils + ../pyams_viewlet + ../pyams_zmi + ../pyams_zmq + +parts = + package + i18n + pyflakes + test + +[package] +recipe = zc.recipe.egg +eggs = + pyams_mail + pyams_scheduler + pyams_zmq + pyramid + zope.component + zope.interface + +[i18n] +recipe = zc.recipe.egg +eggs = + babel + lingua + +[pyflakes] +recipe = zc.recipe.egg +eggs = pyflakes +scripts = pyflakes +entry-points = pyflakes=pyflakes.scripts.pyflakes:main +initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"] + +[pyflakesrun] +recipe = collective.recipe.cmd +on_install = true +cmds = ${buildout:develop}/bin/${pyflakes:scripts} + +[test] +recipe = zc.recipe.testrunner +eggs = pyams_scheduler [test] + +[versions] +pyams_scheduler = 0.1.0 diff -r 000000000000 -r 48483b0b26fa docs/HISTORY.txt diff -r 000000000000 -r 48483b0b26fa docs/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/README.txt Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,50 @@ +======================= +pyams_scheduler package +======================= + +.. contents:: + +What is pyams_scheduler? +======================== + +pyams_scheduler is a package which is part of the PyAMS framework and is targeted to the Pyramid environment. + +This package allows to define tasks which will be scheduled and run asynchronously on a regular basis. + +Scheduling is based on the APScheduler package, and tasks can be scheduled using cron-style, "at" or interval +based scheduling settings. But tasks definitions are stored in the ZODB and they can be managed easily through +a simple web interface; synchronisation between the web process and the scheduler background process is done +using ØMQ messages. + +Tasks can keep an history log in the ZODB; these logs can also be sent by mail, on each run or when errors +are detected. + + +Starting scheduler +================== + +The APScheduler process is started automatically on package include. + +The only thing you have to do is to create an application setting in your INI file, like this: + + pyams_scheduler.tcp_handler = 127.0.0.1:5555 + +This will define the address and port of the listening ØMQ process. Without this setting, the scheduler won't +be started. + + +Adding tasks +============ + +Three task types are defined into PyAMS scheduler package: + + - an URL caller task, which can be used to call any HTTP based URL + + - an SSH command task, which can be used to start local or remote commands + + - a ZODB packing task, which can be used to pack a ZODB. + +You can also register and add your own tasks, which will be added through PyAMS management interface. + +Each task as a set of common properties which are used to name the task, define the scheduling mode and reports +management and activate and schedule the task, as well as settings specific to the given task. diff -r 000000000000 -r 48483b0b26fa setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,77 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +This module contains pyams_scheduler package +""" +import os +from setuptools import setup, find_packages + +DOCS = os.path.join(os.path.dirname(__file__), + 'docs') + +README = os.path.join(DOCS, 'README.txt') +HISTORY = os.path.join(DOCS, 'HISTORY.txt') + +version = '0.1.0' +long_description = open(README).read() + '\n\n' + open(HISTORY).read() + +tests_require = [] + +setup(name='pyams_scheduler', + version=version, + description="PyAMS tasks scheduler", + long_description=long_description, + classifiers=[ + "License :: OSI Approved :: Zope Public License", + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='Pyramid PyAMS scheduler', + author='Thierry Florac', + author_email='tflorac@ulthar.net', + url='http://hg.ztfy.org/pyams/pyams_scheduler', + license='ZPL', + packages=find_packages('src'), + package_dir={'': 'src'}, + namespace_packages=[], + include_package_data=True, + package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']}, + zip_safe=False, + # uncomment this to be able to run tests with setup.py + test_suite="pyams_scheduler.tests.test_utilsdocs.test_suite", + tests_require=tests_require, + extras_require=dict(test=tests_require), + install_requires=[ + 'setuptools', + # -*- Extra requirements: -*- + 'APScheduler', + 'paramiko', + 'persistent', + 'pyams_form', + 'pyams_pagelet', + 'pyams_skin', + 'pyams_template', + 'pyams_utils', + 'pyams_viewlet', + 'pyams_zmq', + 'pyramid', + 'pyramid_mailer', + 'zope.component', + 'zope.interface', + ], + entry_points=""" + # -*- Entry points: -*- + """, + ) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/__init__.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,26 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +from pyramid.i18n import TranslationStringFactory +_ = TranslationStringFactory('pyams_scheduler') + + +def includeme(config): + """Pyramid include + + Split in another package to remove cyclic dependencies with TranslationStringFactory + """ + from .include import include_package + include_package(config) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/configure.zcml Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/doctests/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/doctests/README.txt Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,3 @@ +======================= +pyams_scheduler package +======================= diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/include.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/include.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,76 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import atexit +import logging +logger = logging.getLogger('PyAMS (scheduler') + +# import interfaces +from pyams_scheduler.interfaces import SCHEDULER_HANDLER_KEY, SCHEDULER_NAME +from pyams_utils.interfaces import PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME +from pyams_utils.interfaces.zeo import IZEOConnection + +# import packages +from pyams_scheduler.process import SchedulerProcess, SchedulerMessageHandler +from pyams_utils.zodb import get_connection_from_settings +from pyams_zmq.process import process_exit_func + + +def include_package(config): + """Pyramid package include""" + + # add translations + config.add_translation_dirs('pyams_scheduler:locales') + + # load registry components + try: + import pyams_zmi + except ImportError: + config.scan(ignore='pyams_scheduler.zmi') + else: + config.scan() + + if hasattr(config, 'load_zcml'): + config.load_zcml('configure.zcml') + + start_handler = config.registry.settings.get(SCHEDULER_HANDLER_KEY, False) + if start_handler: + # create scheduler process + process = SchedulerProcess(start_handler, SchedulerMessageHandler, config.registry) + # get database connection + connection = get_connection_from_settings(config.registry.settings) + root = connection.root() + # get application + application_name = config.registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY, + PYAMS_APPLICATION_DEFAULT_NAME) + application = root.get(application_name) + sm = application.getSiteManager() + scheduler_util = sm.get(SCHEDULER_NAME) + zeo_connection = sm.getUtility(IZEOConnection, name=scheduler_util.zeo_connection) + # load tasks + for task in scheduler_util.values(): + trigger = task.get_trigger(config.registry) + logger.debug("Adding scheduler job for task '{0.name}'".format(task)) + process.scheduler.add_job(task, trigger, + id=str(task.internal_id), + name=task.name, + kwargs={'zeo_settings': zeo_connection.get_settings(), + 'registry': config.registry}) + logger.debug("Starting tasks scheduler {0!r}".format(process)) + # start process + process.start() + if process.is_alive(): + atexit.register(process_exit_func, process=process) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/interfaces/__init__.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,381 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.interface.interfaces import IObjectEvent, ObjectEvent + +# import packages +from zope.container.constraints import contains, containers +from zope.interface import implementer, Interface, Attribute +from zope.schema import Date, Datetime, Choice, Text, TextLine, Bool, Int, Float, List, Object, Bytes + +from pyams_scheduler import _ + + +# +# Scheduler events +# + +class IBeforeRunJobEvent(IObjectEvent): + """Interface for events notified before a job is run""" + + +@implementer(IBeforeRunJobEvent) +class BeforeRunJobEvent(ObjectEvent): + """Before run job event""" + + +class IAfterRunJobEvent(IObjectEvent): + """Interface for events notified after a job is run""" + + status = Attribute("Job execution status") + + +@implementer(IAfterRunJobEvent) +class AfterRunJobEvent(ObjectEvent): + """After run job event""" + + def __init__(self, object, status): + super(AfterRunJobEvent, self).__init__(object) + self.status = status + + +# +# Task history item interface +# + +class ITaskHistoryContainer(Interface): + """Task history container interface""" + + contains('pyams_scheduler.interfaces.ITaskHistory') + + +class ITaskHistory(Interface): + """Scheduler task history item interface""" + + containers(ITaskHistoryContainer) + + date = Datetime(title="Execution date", + required=True) + + status = Choice(title="Execution status", + values=('OK', 'Warning', 'Error', 'Empty')) + + report = Text(title="Execution report", + required=True) + + +# +# Scheduler interface +# + + +SCHEDULER_NAME = 'Tasks scheduler' + +SCHEDULER_HANDLER_KEY = 'pyams_scheduler.tcp_handler' + +SCHEDULER_JOBSTORE_KEY = 'pyams_scheduler.jobs' + + +class ISchedulerHandler(Interface): + """Scheduler managemer marker interface""" + + +class IScheduler(IAttributeAnnotatable): + """Scheduler interface""" + + contains('pyams_scheduler.interfaces.ITask') + + zeo_connection = Choice(title=_("ZEO connection name"), + description=_("Name of ZEO connection utility defining scheduler connection"), + required=True, + vocabulary="PyAMS ZEO connections") + + report_mailer = Choice(title=_("Reports mailer"), + description=_("Mail delivery utility used to send mails"), + required=False, + vocabulary='PyAMS mailer utilities') + + report_source = TextLine(title=_("Reports source"), + description=_("Mail address from which reports will be sent"), + required=False) + + internal_id = Attribute("Internal ID") + + def get_task(self, task_id): + """Get task matching given task ID""" + + def get_jobs(self): + """Get text output of running jobs""" + + tasks = List(title=_("Scheduler tasks"), + description=_("List of tasks assigned to this scheduler"), + required=False) + + history = List(title=_("History"), + description=_("Task history"), + value_type=Object(schema=ITaskHistory), + readonly=True) + + +# +# Scheduler task interface +# + +class IJobInfo(Interface): + """Job interface""" + + id = TextLine(title="Job ID") + + next_run_time = Float(title="Job next run time") + + job_state = Bytes(title="Job state") + + +class ITaskInfo(Interface): + """Scheduler task interface""" + + containers(IScheduler) + + name = TextLine(title=_("Task name"), + description=_("Descriptive name given to this task"), + required=True) + + schedule_mode = Choice(title=_("Scheduling mode"), + description=_("Scheduling mode defines how task will be scheduled"), + vocabulary="PyAMS scheduling modes", + required=True) + + report_target = TextLine(title=_("Reports target(s)"), + description=_("Mail address(es) to which execution reports will be sent; you can enter " + "several addresses separated by semicolons"), + required=False) + + errors_target = TextLine(title=_("Errors reports target(s)"), + description=_("Mail address(es) to which error reports will be sent; you can enter " + "several addresses separated by semicolons; keep empty to use normal " + "reports target"), + required=False) + + report_errors_only = Bool(title=_("Only report errors?"), + description=_("If 'Yes', only error reports will be sent to given errors target"), + required=True, + default=False) + + send_empty_reports = Bool(title=_("Send empty reports?"), + description=_("If 'No', empty reports will not be sent by mail"), + required=True, + default=False) + + keep_empty_reports = Bool(title=_("Keep empty reports history?"), + description=_("If 'Yes', empty reports will be kept in task history"), + required=True, + default=False) + + history_duration = Int(title=_("History duration"), + description=_("Number of days during which execution reports are kept in history; enter " + "0 to remove limit"), + required=False) + + history_length = Int(title=_("History max length"), + description=_("Number of execution reports to keep in history; enter 0 to remove limit"), + required=False) + + +class ITask(ITaskInfo, IAttributeAnnotatable): + """Complete task interface""" + + history = List(title=_("History"), + description=_("Task history"), + value_type=Object(schema=ITaskHistory)) + + runnable = Attribute("Is the task runnable ?") + + internal_id = Attribute("Internal ID") + + def get_trigger(self, registry): + """Get scheduler job trigger""" + + def get_scheduling_info(self, registry): + """Get scheduling info""" + + def run(self, report, **kw): + """Launch job execution""" + + def store_report(self, report, status): + """Store execution report in task's history and send it by mail""" + + def send_report(self, report, status, target=None): + """Store execution report in task's history and send it by mail""" + + def reset(self): + """Re-schedule job execution""" + + def launch(self): + """Ask task for immediate execution""" + + +# +# Task scheduling modes interfaces +# + +class ITaskSchedulingMode(Interface): + """Scheduler task scheduling mode""" + + marker_interface = Attribute("Class name of scheduling mode marker interface") + + schema = Attribute("Class name of scheduling mode info interface") + + def get_trigger(self, task): + """Get trigger for the given task""" + + def schedule(self, task, scheduler): + """Add given task to the scheduler""" + + +class ITaskSchedulingMarker(Interface): + """Base interface for task scheduling mode markers""" + + +class IBaseTaskScheduling(Interface): + """Base interface for task scheduling info""" + + active = Bool(title=_("Active task"), + description=_("You can disable a task by selecting 'No'"), + required=True, + default=False) + + start_date = Datetime(title=_("First execution date"), + description=_("Date from which scheduling should start"), + required=False) + + +# Scheduler cron-style tasks interfaces + +SCHEDULER_TASK_CRON_INFO = 'pyams_scheduler.trigger.cron' + + +class ICronTask(Interface): + """Cron-style task marker interface""" + + +class ICronTaskScheduling(IBaseTaskScheduling): + """Base interface for cron-style scheduled tasks""" + + end_date = Datetime(title=_("Last execution date"), + description=_("Date past which scheduling should end"), + required=False) + + year = TextLine(title=_("Years"), + description=_("Years for which to schedule the job"), + required=False, + default='*') + + month = TextLine(title=_("Months"), + description=_("Months (1-12) for which to schedule the job"), + required=False, + default='*') + + day = TextLine(title=_("Month days"), + description=_("Days (1-31) for which to schedule the job"), + required=False, + default='*') + + week = TextLine(title=_("Weeks"), + description=_("Year weeks (1-53) for which to schedule the job"), + required=False, + default='*') + + day_of_week = TextLine(title=_("Week days"), + description=_("Week days (0-6, with 0 as monday) for which to schedule the job"), + required=False, + default='*') + + hour = TextLine(title=_("Hours"), + description=_("Hours (0-23) for which to schedule the job"), + required=False, + default='*') + + minute = TextLine(title=_("Minutes"), + description=_("Minutes (0-59) for which to schedule the job"), + required=False, + default='*') + + second = TextLine(title=_("Seconds"), + description=_("Seconds (0-59) for which to schedule the job"), + required=False, + default='0') + + +# Scheduler date-style tasks interface + +SCHEDULER_TASK_DATE_INFO = 'pyams_scheduler.trigger.date' + + +class IDateTask(Interface): + """Date-style task marker interface""" + + +class IDateTaskScheduling(IBaseTaskScheduling): + """Base interface for date-style scheduled tasks""" + + start_date = Datetime(title=_("Execution date"), + description=_("Date on which execution should start"), + required=True) + + +# Scheduler loop-style tasks interface + +SCHEDULER_TASK_LOOP_INFO = 'pyams_scheduler.trigger.loop' + + +class ILoopTask(Interface): + """Loop-style task marker interface""" + + +class ILoopTaskScheduling(IBaseTaskScheduling): + """Base interface for loop-style scheduled tasks""" + + end_date = Datetime(title=_("Last execution date"), + description=_("Date past which scheduling should end"), + required=False) + + weeks = Int(title=_("Weeks interval"), + description=_("Number of weeks between executions"), + required=True, + default=0) + + days = Int(title=_("Days interval"), + description=_("Number of days between executions"), + required=True, + default=0) + + hours = Int(title=_("Hours interval"), + description=_("Number of hours between executions"), + required=True, + default=0) + + minutes = Int(title=_("Minutes interval"), + description=_("Number of minutes between executions"), + required=True, + default=1) + + seconds = Int(title=_("Seconds interval"), + description=_("Number of seconds between executions"), + required=True, + default=0) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/interfaces/ssh.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/interfaces/ssh.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,62 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces import ITask + +# import packages +from zope.interface import invariant, Interface, Invalid +from zope.schema import TextLine, Int, Password + +from pyams_scheduler import _ + + +class ISSHCallerTaskInfo(Interface): + """SSH caller task info""" + + hostname = TextLine(title=_("Target hostname of IP address"), + description=_("Enter hostname or address of a remote hots; keep empty for local server host"), + required=False) + + port = Int(title=_("SSH port number"), + default=22, + required=False) + + username = TextLine(title=_("User name"), + required=False) + + private_key = TextLine(title=_("Private key filename"), + description=_("Enter name of private key file; use '~' to identify " + "running server user home directory, as in ~/.ssh/id_rsa"), + required=False) + + password = Password(title=_("Password"), + description=_("If not using private key, you must provide user's password"), + required=False) + + cmdline = TextLine(title=_("Command line"), + description=_("Enter command line, using absolute path names"), + required=True) + + @invariant + def check_remote_host(self): + if self.hostname and (bool(self.private_key) == bool(self.password)): + raise Invalid(_("You must provide a private key filename OR a password when defining remote tasks")) + + +class ISSHCallerTask(ITask, ISSHCallerTaskInfo): + """SSH caller interface""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/interfaces/url.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/interfaces/url.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,83 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from urllib import parse + +# import interfaces +from pyams_scheduler.interfaces import ITask + +# import packages +from zope.interface import invariant, Interface, Invalid +from zope.schema import URI, TextLine, Bool, Password, Int + +from pyams_scheduler import _ + + +class IURLCallerTaskInfo(Interface): + """URL caller task info""" + + url = URI(title=_("Target URI"), + description=_("Full URI of remote service"), + required=True) + + username = TextLine(title=_("User name"), + description=_("Target login"), + required=False) + + password = Password(title=_("Password"), + description=_("Target password"), + required=False) + + proxy_server = TextLine(title=_("Proxy server"), + description=_("Proxy server name"), + required=False) + + proxy_port = Int(title=_("Proxy port"), + description=_("Proxy server port"), + required=False, + default=8080) + + remote_dns = Bool(title=_("Use remote DNS ?"), + description=_("If 'Yes', remote DNS queries will be done by proxy server"), + required=True, + default=True) + + proxy_username = TextLine(title=_("Proxy user name"), + required=False) + + proxy_password = Password(title=_("Proxy password"), + required=False) + + connection_timeout = Int(title=_("Connection timeout"), + description=_("Connection timeout, in seconds; keep empty to use system's " + "default, which is also none by default"), + required=False, + default=30) + + @invariant + def check_hostname(self): + parser = parse.urlparse(self.url) + if not parser.netloc: + raise Invalid(_("Missing hostname!")) + + @invariant + def check_proxy(self): + if self.proxy_server and not self.proxy_port: + raise Invalid(_("Proxy server defined without proxy port!")) + + +class IURLCallerTask(ITask, IURLCallerTaskInfo): + """URL caller interface""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/interfaces/zodb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/interfaces/zodb.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,43 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces import ITask + +# import packages +from zope.interface import Interface +from zope.schema import Choice, Int + +from pyams_scheduler import _ + + +class IZODBPackingTaskInfo(Interface): + """ZODB packing task info""" + + zeo_connection = Choice(title=_("ZEO connection name"), + description=_("Name of ZEO connection utility pointing to packed database"), + required=True, + vocabulary="PyAMS ZEO connections") + + pack_time = Int(title=_("Maximum transactions age"), + description=_("Transactions older than this age, in days, will be removed"), + required=True, + default=0) + + +class IZODBPackingTask(ITask, IZODBPackingTaskInfo): + """ZODB packing task interface""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/locales/fr/LC_MESSAGES/pyams_scheduler.mo Binary file src/pyams_scheduler/locales/fr/LC_MESSAGES/pyams_scheduler.mo has changed diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/locales/fr/LC_MESSAGES/pyams_scheduler.po --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/locales/fr/LC_MESSAGES/pyams_scheduler.po Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,615 @@ +# +# French translations for PACKAGE package +# This file is distributed under the same license as the PACKAGE package. +# Thierry Florac , 2015. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-03-11 10:38+0100\n" +"PO-Revision-Date: 2015-03-11 10:38+0100\n" +"Last-Translator: Thierry Florac \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Lingua 3.8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: src/pyams_scheduler/trigger.py:81 +msgid "Task is not configured for cron-style scheduling!" +msgstr "Cette tâche n'est pas configurée en mode 'cron' !" + +#: src/pyams_scheduler/trigger.py:126 +msgid "Task is not configured for date-style scheduling!" +msgstr "Cette tâche n'est pas configurée en mode 'date' !" + +#: src/pyams_scheduler/trigger.py:168 +msgid "Task is not configured for loop-style scheduling!" +msgstr "Cette tâche n'est pas configurée en mode 'interval' !" + +#: src/pyams_scheduler/zmi/url.py:45 +msgid "Add URL caller task..." +msgstr "Ajouter un appel d'URL..." + +#: src/pyams_scheduler/zmi/url.py:56 src/pyams_scheduler/zmi/task.py:64 +msgid "Add URL caller task" +msgstr "Ajout d'une tâche d'appel d'URL" + +#: src/pyams_scheduler/zmi/url.py:78 src/pyams_scheduler/zmi/ssh.py:78 +#: src/pyams_scheduler/zmi/zodb.py:78 +msgid "Edit task settings" +msgstr "Paramètres de la tâche" + +#: src/pyams_scheduler/zmi/url.py:76 src/pyams_scheduler/zmi/task.py:104 +#: src/pyams_scheduler/zmi/task.py:132 src/pyams_scheduler/zmi/task.py:201 +#: src/pyams_scheduler/zmi/task.py:277 src/pyams_scheduler/zmi/ssh.py:76 +#: src/pyams_scheduler/zmi/zodb.py:76 +#, python-format +msgid "Scheduler task: {0}" +msgstr "Tâche planifiée : {0}" + +#: src/pyams_scheduler/zmi/task.py:63 src/pyams_scheduler/zmi/scheduler.py:81 +#: src/pyams_scheduler/zmi/scheduler.py:225 +msgid "Tasks scheduler" +msgstr "Tâches planifiées" + +#: src/pyams_scheduler/zmi/task.py:106 +msgid "Edit task properties" +msgstr "Propriétés de la tâche" + +#: src/pyams_scheduler/zmi/task.py:134 +#: src/pyams_scheduler/zmi/scheduler.py:134 +msgid "Schedule task" +msgstr "Planifier la tâche" + +#: src/pyams_scheduler/zmi/task.py:204 +msgid "Execute task" +msgstr "Exécuter la tâche" + +#: src/pyams_scheduler/zmi/task.py:251 +msgid "Executing and debugging task" +msgstr "Exécuter ou déboguer une tâche" + +#: src/pyams_scheduler/zmi/task.py:252 +msgid "" +"You can choose to execute the task in 'normal' mode or in 'debug' mode.\n" +"\n" +"In normal mode, the task is scheduled in a standard way and run in a " +"background process (after 5 seconds).\n" +"\n" +"In debug mode, the task is run in the context of the main application " +"process; the goal of this mode\n" +"is to allow a developer to insert breakpoints.\n" +"\n" +"**WARNING**: in both mode, the task will be executed even if it's disabled " +"in it's scheduling settings!" +msgstr "" +"Vous pouvez choisir d'exécuter cette tâche en mode 'normal' ou en mode 'debug'.\n" +"\n" +"En mode 'normal', la tâche est planifiée normalement pour être exécutée au sein " +"du processus de planification en arrière plan (passé un délai de 5 secondes).\n" +"\n" +"En mode 'debug', la tâche est exécutée dans le contexte du processus principal " +"du serveur web : le but de ce mode est de permettre aux développeurs de placer " +"des points d'arrêt pour faciliter le déboguage.\n" +"\n" +"**ATTENTION** : quel que soit le mode, la tâche sera exécutée même si elle a été " +"désactivée dans ses paramètres de planification !" + +#: src/pyams_scheduler/zmi/task.py:279 src/pyams_scheduler/zmi/task.py:295 +#: src/pyams_scheduler/interfaces/__init__.py:131 +#: src/pyams_scheduler/interfaces/__init__.py:204 +msgid "Task history" +msgstr "Historique de la tâche" + +#: src/pyams_scheduler/zmi/task.py:318 +msgid "Date" +msgstr "Date" + +#: src/pyams_scheduler/zmi/task.py:189 +msgid "Close" +msgstr "Fermer" + +#: src/pyams_scheduler/zmi/task.py:190 +msgid "Run in debug mode" +msgstr "Exécuter en mode 'debug'" + +#: src/pyams_scheduler/zmi/task.py:191 +msgid "Run in normal mode" +msgstr "Exécuter la tâche" + +#: src/pyams_scheduler/zmi/task.py:94 +msgid "Specified task name is already used!" +msgstr "Le nom indiqué est déjà utilisé !" + +#: src/pyams_scheduler/zmi/task.py:236 +msgid "Task scheduled in normal mode will start in 5 seconds..." +msgstr "La tâche a été planifiée et sera exécutée dans 5 secondes..." + +#: src/pyams_scheduler/zmi/task.py:242 +msgid "Task run in debug mode. Please check your console..." +msgstr "La tâche a été exécutée en mode 'debug'. Veuillez vérifier votre console..." + +#: src/pyams_scheduler/zmi/scheduler.py:90 +#: src/pyams_scheduler/zmi/scheduler.py:226 +msgid "Scheduled tasks" +msgstr "Tâches planifiées" + +#: src/pyams_scheduler/zmi/scheduler.py:110 +msgid "Name" +msgstr "Nom" + +#: src/pyams_scheduler/zmi/scheduler.py:120 +msgid "Task settings" +msgstr "Paramètres de la tâche" + +#: src/pyams_scheduler/zmi/scheduler.py:148 +msgid "Run task" +msgstr "Exécuter en arrière plan" + +#: src/pyams_scheduler/zmi/scheduler.py:162 +msgid "Task run history" +msgstr "Historique" + +#: src/pyams_scheduler/zmi/scheduler.py:175 +msgid "Delete task" +msgstr "Supprimer la tâche" + +#: src/pyams_scheduler/zmi/scheduler.py:234 +msgid "Properties..." +msgstr "Propriétés..." + +#: src/pyams_scheduler/zmi/scheduler.py:251 +msgid "Update tasks scheduler properties" +msgstr "Propriétés du planificateur de tâches" + +#: src/pyams_scheduler/zmi/scheduler.py:269 +msgid "Active jobs..." +msgstr "Tâches actives..." + +#: src/pyams_scheduler/zmi/scheduler.py:287 +msgid "Display scheduler active jobs" +msgstr "Tâches actuellement planifiées" + +#: src/pyams_scheduler/zmi/scheduler.py:295 +msgid "Scheduler jobs" +msgstr "Jobs en cours de planification" + +#: src/pyams_scheduler/zmi/scheduler.py:312 +msgid "Job name" +msgstr "Nom du job" + +#: src/pyams_scheduler/zmi/scheduler.py:323 +msgid "ID" +msgstr "ID" + +#: src/pyams_scheduler/zmi/scheduler.py:334 +msgid "Trigger" +msgstr "Déclencheur" + +#: src/pyams_scheduler/zmi/scheduler.py:345 +msgid "Next run" +msgstr "Prochain lancement" + +#: src/pyams_scheduler/zmi/scheduler.py:188 +msgid "No provided object_name argument!" +msgstr "Argument 'object_name' non fourni !" + +#: src/pyams_scheduler/zmi/scheduler.py:192 +msgid "Given task name doesn't exist!" +msgstr "Le nom de la tâche indiquée n'existe pas !" + +#: src/pyams_scheduler/zmi/ssh.py:45 +msgid "Add SSH command..." +msgstr "Ajouter une commande SSH..." + +#: src/pyams_scheduler/zmi/ssh.py:56 +msgid "Add SSH command task" +msgstr "Ajout d'une commande SSH" + +#: src/pyams_scheduler/zmi/zodb.py:45 +msgid "Add ZODB packing task..." +msgstr "Ajouter un compacteur ZODB..." + +#: src/pyams_scheduler/zmi/zodb.py:56 +msgid "Add ZODB packing task" +msgstr "Ajout d'une commande de compactage de ZODB" + +#: src/pyams_scheduler/interfaces/url.py:32 +msgid "Target URI" +msgstr "URI cible" + +#: src/pyams_scheduler/interfaces/url.py:33 +msgid "Full URI of remote service" +msgstr "URI complète du service" + +#: src/pyams_scheduler/interfaces/url.py:36 +#: src/pyams_scheduler/interfaces/ssh.py:39 +msgid "User name" +msgstr "Code utilisateur" + +#: src/pyams_scheduler/interfaces/url.py:37 +msgid "Target login" +msgstr "Login utilisé sur le service" + +#: src/pyams_scheduler/interfaces/url.py:40 +#: src/pyams_scheduler/interfaces/ssh.py:47 +msgid "Password" +msgstr "Mot de passe" + +#: src/pyams_scheduler/interfaces/url.py:41 +msgid "Target password" +msgstr "Mot de passe utilisé sur le service" + +#: src/pyams_scheduler/interfaces/url.py:44 +msgid "Proxy server" +msgstr "Serveur proxy" + +#: src/pyams_scheduler/interfaces/url.py:45 +msgid "Proxy server name" +msgstr "Nom du serveur mandataire" + +#: src/pyams_scheduler/interfaces/url.py:48 +msgid "Proxy port" +msgstr "Port du proxy" + +#: src/pyams_scheduler/interfaces/url.py:49 +msgid "Proxy server port" +msgstr "Numéro de port du serveur mandataire" + +#: src/pyams_scheduler/interfaces/url.py:53 +msgid "Use remote DNS ?" +msgstr "DNS via le proxy ?" + +#: src/pyams_scheduler/interfaces/url.py:54 +msgid "If 'Yes', remote DNS queries will be done by proxy server" +msgstr "Si 'oui', les requêtes DNS distantes seront exécutées par le serveur mandataire" + +#: src/pyams_scheduler/interfaces/url.py:58 +msgid "Proxy user name" +msgstr "Utilisateur du proxy" + +#: src/pyams_scheduler/interfaces/url.py:61 +msgid "Proxy password" +msgstr "Mot de passe du proxy" + +#: src/pyams_scheduler/interfaces/url.py:64 +msgid "Connection timeout" +msgstr "Délai de connexion" + +#: src/pyams_scheduler/interfaces/url.py:65 +msgid "" +"Connection timeout, in seconds; keep empty to use system's default, which is " +"also none by default" +msgstr "" +"Délai de connexion, en secondes ; laisser cette valeur vide pour utiliser la valeur " +"par défaut du système, qui est également nulle" + +#: src/pyams_scheduler/interfaces/url.py:74 +msgid "Missing hostname!" +msgstr "Nom de serveur absent !" + +#: src/pyams_scheduler/interfaces/url.py:79 +msgid "Proxy server defined without proxy port!" +msgstr "Serveur mandataire défini sans numéro de port indiqué !" + +#: src/pyams_scheduler/interfaces/__init__.py:104 +#: src/pyams_scheduler/interfaces/zodb.py:31 +msgid "ZEO connection name" +msgstr "Connexion ZEO" + +#: src/pyams_scheduler/interfaces/__init__.py:105 +msgid "Name of ZEO connection utility defining scheduler connection" +msgstr "Nom de la connexion ZEO utilisée par le service de planification" + +#: src/pyams_scheduler/interfaces/__init__.py:109 +msgid "Reports mailer" +msgstr "Gestionnaire d'envois" + +#: src/pyams_scheduler/interfaces/__init__.py:110 +msgid "Mail delivery utility used to send mails" +msgstr "Nom du service de messagerie utilisé pour gérer les envois des rapports d'exécution" + +#: src/pyams_scheduler/interfaces/__init__.py:114 +msgid "Reports source" +msgstr "Source des rapports" + +#: src/pyams_scheduler/interfaces/__init__.py:115 +msgid "Mail address from which reports will be sent" +msgstr "Adresse source à partir de laquelle seront effectués les envois des rapports d'exécution" + +#: src/pyams_scheduler/interfaces/__init__.py:126 +msgid "Scheduler tasks" +msgstr "Tâches planifiées" + +#: src/pyams_scheduler/interfaces/__init__.py:127 +msgid "List of tasks assigned to this scheduler" +msgstr "Liste des tâches assignées au service de planification" + +#: src/pyams_scheduler/interfaces/__init__.py:130 +#: src/pyams_scheduler/interfaces/__init__.py:203 +msgid "History" +msgstr "Historique" + +#: src/pyams_scheduler/interfaces/__init__.py:155 +msgid "Task name" +msgstr "Nom de la tâche" + +#: src/pyams_scheduler/interfaces/__init__.py:156 +msgid "Descriptive name given to this task" +msgstr "Nom unique donné à la tâche" + +#: src/pyams_scheduler/interfaces/__init__.py:159 +msgid "Scheduling mode" +msgstr "Mode de planification" + +#: src/pyams_scheduler/interfaces/__init__.py:160 +msgid "Scheduling mode defines how task will be scheduled" +msgstr "Le mode de planification détermine la façon dont la tâche sera planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:164 +msgid "Reports target(s)" +msgstr "Destinataire(s)" + +#: src/pyams_scheduler/interfaces/__init__.py:165 +msgid "" +"Mail address(es) to which execution reports will be sent; you can enter " +"several addresses separated by semicolons" +msgstr "" +"Adresse(e) de messagerie des destinataires des rapports d'exécution ; vous " +"pouvez indiquer plusieurs adresses en les séparant par un point-virgule" + +#: src/pyams_scheduler/interfaces/__init__.py:169 +msgid "Errors reports target(s)" +msgstr "Destinataire(s) des erreurs" + +#: src/pyams_scheduler/interfaces/__init__.py:170 +msgid "" +"Mail address(es) to which error reports will be sent; you can enter several " +"addresses separated by semicolons; keep empty to use normal reports target" +msgstr "" +"Vous pouvez choisir des destinataires spécifiques pour les rapports d'erreurs ; " +"vous pouvez indiquer plusieurs adresses séparées par un point-virgule ; si vous laissez " +"ce champ vide, les erreurs seront adressées aux destinataires normaux" + +#: src/pyams_scheduler/interfaces/__init__.py:175 +msgid "Only report errors?" +msgstr "Rapports d'erreurs seulement ?" + +#: src/pyams_scheduler/interfaces/__init__.py:176 +msgid "If 'Yes', only error reports will be sent to given errors target" +msgstr "Si 'oui', seuls les rapports d'exécution en erreur seront envoyés aux destinataires indiqués" + +#: src/pyams_scheduler/interfaces/__init__.py:180 +msgid "Send empty reports?" +msgstr "Envoyer des rapports vides ?" + +#: src/pyams_scheduler/interfaces/__init__.py:181 +msgid "If 'No', empty reports will not be sent by mail" +msgstr "Si 'non', les rapports d'exécution vides ne seront pas envoyés par la messagerie" + +#: src/pyams_scheduler/interfaces/__init__.py:185 +msgid "Keep empty reports history?" +msgstr "Conserver les rapports vides ?" + +#: src/pyams_scheduler/interfaces/__init__.py:186 +msgid "If 'Yes', empty reports will be kept in task history" +msgstr "Si 'oui', les rapports d'exécution vides seront conservés dans l'historique de la tâche" + +#: src/pyams_scheduler/interfaces/__init__.py:190 +msgid "History duration" +msgstr "Durée de conservation" + +#: src/pyams_scheduler/interfaces/__init__.py:191 +msgid "" +"Number of days during which execution reports are kept in history; enter 0 " +"to remove limit" +msgstr "" +"Nombre de jours pendant lesquels les rapports d'exécution seront conservés dans l'historique ; " +"laissez cette option vide pour ne pas appliquer cette limite" + +#: src/pyams_scheduler/interfaces/__init__.py:195 +msgid "History max length" +msgstr "Longueur de l'historique" + +#: src/pyams_scheduler/interfaces/__init__.py:196 +msgid "Number of execution reports to keep in history; enter 0 to remove limit" +msgstr "Nombre maximal de rapports conservés dans l'historique ; laissez cette option " +"vide pour ne pas appliquer cette limite" + +#: src/pyams_scheduler/interfaces/__init__.py:258 +msgid "Active task" +msgstr "Tâche active" + +#: src/pyams_scheduler/interfaces/__init__.py:259 +msgid "You can disable a task by selecting 'No'" +msgstr "Une tâche inactive reste visible dans les travaux planifiés mais n'est pas exécutée" + +#: src/pyams_scheduler/interfaces/__init__.py:263 +msgid "First execution date" +msgstr "À partir du..." + +#: src/pyams_scheduler/interfaces/__init__.py:264 +msgid "Date from which scheduling should start" +msgstr "Date à partir de laquelle la tâche sera planifiée ; laisser vide pour une prise en compte immédiate" + +#: src/pyams_scheduler/interfaces/__init__.py:280 +#: src/pyams_scheduler/interfaces/__init__.py:354 +msgid "Last execution date" +msgstr "Jusqu'au..." + +#: src/pyams_scheduler/interfaces/__init__.py:281 +#: src/pyams_scheduler/interfaces/__init__.py:355 +msgid "Date past which scheduling should end" +msgstr "Date au-delà de laquelle la tâche ne sera plus planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:284 +msgid "Years" +msgstr "Années" + +#: src/pyams_scheduler/interfaces/__init__.py:285 +msgid "Years for which to schedule the job" +msgstr "Année pendant lesquelles la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:289 +msgid "Months" +msgstr "Mois" + +#: src/pyams_scheduler/interfaces/__init__.py:290 +msgid "Months (1-12) for which to schedule the job" +msgstr "Mois (1 à 12) pendant lesquels la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:294 +msgid "Month days" +msgstr "Jours du mois" + +#: src/pyams_scheduler/interfaces/__init__.py:295 +msgid "Days (1-31) for which to schedule the job" +msgstr "Jours du mois (1 à 31) pendant lesquels la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:299 +msgid "Weeks" +msgstr "Semaines" + +#: src/pyams_scheduler/interfaces/__init__.py:300 +msgid "Year weeks (1-53) for which to schedule the job" +msgstr "Semaines de l'année (1 à 53) pendant lesquelles la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:304 +msgid "Week days" +msgstr "Jours de la semaine" + +#: src/pyams_scheduler/interfaces/__init__.py:305 +msgid "Week days (0-6, with 0 as monday) for which to schedule the job" +msgstr "Jours de la semaine (0 à 6, 0 étant le dimanche) pendant lesquels la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:309 +msgid "Hours" +msgstr "Heures" + +#: src/pyams_scheduler/interfaces/__init__.py:310 +msgid "Hours (0-23) for which to schedule the job" +msgstr "Heures du jour (0 à 23) auxquelles la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:314 +msgid "Minutes" +msgstr "Minutes" + +#: src/pyams_scheduler/interfaces/__init__.py:315 +msgid "Minutes (0-59) for which to schedule the job" +msgstr "Minutes (0 à 59) auxquelles la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:319 +msgid "Seconds" +msgstr "Secondes" + +#: src/pyams_scheduler/interfaces/__init__.py:320 +msgid "Seconds (0-59) for which to schedule the job" +msgstr "Secondes (0 à 59) auxquelles la tâche est planifiée" + +#: src/pyams_scheduler/interfaces/__init__.py:337 +msgid "Execution date" +msgstr "Date d'exécution" + +#: src/pyams_scheduler/interfaces/__init__.py:338 +msgid "Date on which execution should start" +msgstr "Date à laquelle la tâche sera exécutée" + +#: src/pyams_scheduler/interfaces/__init__.py:358 +msgid "Weeks interval" +msgstr "Semaines" + +#: src/pyams_scheduler/interfaces/__init__.py:359 +msgid "Number of weeks between executions" +msgstr "Nombre de semaines entre deux exécutions" + +#: src/pyams_scheduler/interfaces/__init__.py:363 +msgid "Days interval" +msgstr "Jours" + +#: src/pyams_scheduler/interfaces/__init__.py:364 +msgid "Number of days between executions" +msgstr "Nombre de jours entre deux exécutions" + +#: src/pyams_scheduler/interfaces/__init__.py:368 +msgid "Hours interval" +msgstr "Heures" + +#: src/pyams_scheduler/interfaces/__init__.py:369 +msgid "Number of hours between executions" +msgstr "Nombre d'heures entre deux exécutions" + +#: src/pyams_scheduler/interfaces/__init__.py:373 +msgid "Minutes interval" +msgstr "Minutes" + +#: src/pyams_scheduler/interfaces/__init__.py:374 +msgid "Number of minutes between executions" +msgstr "Nombre de minutes entre deux exécutions" + +#: src/pyams_scheduler/interfaces/__init__.py:378 +msgid "Seconds interval" +msgstr "Secondes" + +#: src/pyams_scheduler/interfaces/__init__.py:379 +msgid "Number of seconds between executions" +msgstr "Nombre de secondes entre deux exécutions" + +#: src/pyams_scheduler/interfaces/ssh.py:31 +msgid "Target hostname of IP address" +msgstr "Nom ou adresse IP" + +#: src/pyams_scheduler/interfaces/ssh.py:32 +msgid "" +"Enter hostname or address of a remote hots; keep empty for local server host" +msgstr "" +"Nom de machine ou adresse IP d'un serveur distant ; laissez ce paramètre vide pour exécuter une commande locale" + +#: src/pyams_scheduler/interfaces/ssh.py:35 +msgid "SSH port number" +msgstr "Port SSH" + +#: src/pyams_scheduler/interfaces/ssh.py:42 +msgid "Private key filename" +msgstr "Fichier de clé privée" + +#: src/pyams_scheduler/interfaces/ssh.py:43 +msgid "" +"Enter name of private key file; use '~' to identify running server user home " +"directory, as in ~/.ssh/id_rsa" +msgstr "" +"Dans le cas d'une connexion SSH, vous pouvez indiquer le nom du fichier contenant la clé privée ; " +"indiquez '~' pour indiquer le répertoire personnel de l'utilisateur qui exécute le serveur web, " +"comme dans '~/.sh/id_rsa'" + +#: src/pyams_scheduler/interfaces/ssh.py:48 +msgid "If not using private key, you must provide user's password" +msgstr "Vous devez indiquer un mot de passe si vous n'utilisez pas de clé privée..." + +#: src/pyams_scheduler/interfaces/ssh.py:51 +msgid "Command line" +msgstr "Ligne de commande" + +#: src/pyams_scheduler/interfaces/ssh.py:52 +msgid "Enter command line, using absolute path names" +msgstr "Chemin d'accès complet de la ligne de commande exécutée" + +#: src/pyams_scheduler/interfaces/ssh.py:58 +msgid "" +"You must provide a private key filename OR a password when defining remote " +"tasks" +msgstr "" +"Vous devez fournir une clé privée OU un mot de passe pour vous connecter sur un serveur distant !" + +#: src/pyams_scheduler/interfaces/zodb.py:32 +msgid "Name of ZEO connection utility pointing to packed database" +msgstr "Nom de la connexion ZEO correspondant à la base de données à compacter" + +#: src/pyams_scheduler/interfaces/zodb.py:36 +msgid "Maximum transactions age" +msgstr "Âge des transactions" + +#: src/pyams_scheduler/interfaces/zodb.py:37 +msgid "Transactions older than this age, in days, will be removed" +msgstr "Les transactions datant d'un nombre de jours supérieur à la valeur indiquée seront supprimées" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/locales/pyams_scheduler.pot --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/locales/pyams_scheduler.pot Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,587 @@ +# +# SOME DESCRIPTIVE TITLE +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2015. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-03-11 10:38+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Lingua 3.8\n" + +#: ./src/pyams_scheduler/trigger.py:81 +msgid "Task is not configured for cron-style scheduling!" +msgstr "" + +#: ./src/pyams_scheduler/trigger.py:126 +msgid "Task is not configured for date-style scheduling!" +msgstr "" + +#: ./src/pyams_scheduler/trigger.py:168 +msgid "Task is not configured for loop-style scheduling!" +msgstr "" + +#: ./src/pyams_scheduler/zmi/url.py:45 +msgid "Add URL caller task..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/url.py:56 ./src/pyams_scheduler/zmi/task.py:64 +msgid "Add URL caller task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/url.py:78 ./src/pyams_scheduler/zmi/ssh.py:78 +#: ./src/pyams_scheduler/zmi/zodb.py:78 +msgid "Edit task settings" +msgstr "" + +#: ./src/pyams_scheduler/zmi/url.py:76 ./src/pyams_scheduler/zmi/task.py:104 +#: ./src/pyams_scheduler/zmi/task.py:132 ./src/pyams_scheduler/zmi/task.py:201 +#: ./src/pyams_scheduler/zmi/task.py:277 ./src/pyams_scheduler/zmi/ssh.py:76 +#: ./src/pyams_scheduler/zmi/zodb.py:76 +#, python-format +msgid "Scheduler task: {0}" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:63 +#: ./src/pyams_scheduler/zmi/scheduler.py:81 +#: ./src/pyams_scheduler/zmi/scheduler.py:225 +msgid "Tasks scheduler" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:106 +msgid "Edit task properties" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:134 +#: ./src/pyams_scheduler/zmi/scheduler.py:134 +msgid "Schedule task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:204 +msgid "Execute task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:251 +msgid "Executing and debugging task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:252 +msgid "" +"You can choose to execute the task in 'normal' mode or in 'debug' mode.\n" +"\n" +"In normal mode, the task is scheduled in a standard way and run in a background process (after 5 seconds).\n" +"\n" +"In debug mode, the task is run in the context of the main application process; the goal of this mode\n" +"is to allow a developer to insert breakpoints.\n" +"\n" +"**WARNING**: in both mode, the task will be executed even if it's disabled in it's scheduling settings!" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:279 ./src/pyams_scheduler/zmi/task.py:295 +#: ./src/pyams_scheduler/interfaces/__init__.py:131 +#: ./src/pyams_scheduler/interfaces/__init__.py:204 +msgid "Task history" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:318 +msgid "Date" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:189 +msgid "Close" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:190 +msgid "Run in debug mode" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:191 +msgid "Run in normal mode" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:94 +msgid "Specified prefix is already used!" +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:236 +msgid "Task scheduled in normal mode will start in 5 seconds..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/task.py:242 +msgid "Task run in debug mode. Please check your console..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:90 +#: ./src/pyams_scheduler/zmi/scheduler.py:226 +msgid "Scheduled tasks" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:110 +msgid "Name" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:120 +msgid "Task settings" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:148 +msgid "Run task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:162 +msgid "Task run history" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:175 +msgid "Delete task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:234 +msgid "Properties..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:251 +msgid "Update tasks scheduler properties" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:269 +msgid "Active jobs..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:287 +msgid "Display scheduler active jobs" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:295 +msgid "Scheduler jobs" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:312 +msgid "Job name" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:323 +msgid "ID" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:334 +msgid "Trigger" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:345 +msgid "Next run" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:188 +msgid "No provided object_name argument!" +msgstr "" + +#: ./src/pyams_scheduler/zmi/scheduler.py:192 +msgid "Given task name doesn't exist!" +msgstr "" + +#: ./src/pyams_scheduler/zmi/ssh.py:45 +msgid "Add SSH command..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/ssh.py:56 +msgid "Add SSH command task" +msgstr "" + +#: ./src/pyams_scheduler/zmi/zodb.py:45 +msgid "Add ZODB packing task..." +msgstr "" + +#: ./src/pyams_scheduler/zmi/zodb.py:56 +msgid "Add ZODB packing task" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:32 +msgid "Target URI" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:33 +msgid "Full URI of remote service" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:36 +#: ./src/pyams_scheduler/interfaces/ssh.py:39 +msgid "User name" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:37 +msgid "Target login" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:40 +#: ./src/pyams_scheduler/interfaces/ssh.py:47 +msgid "Password" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:41 +msgid "Target password" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:44 +msgid "Proxy server" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:45 +msgid "Proxy server name" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:48 +msgid "Proxy port" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:49 +msgid "Proxy server port" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:53 +msgid "Use remote DNS ?" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:54 +msgid "If 'Yes', remote DNS queries will be done by proxy server" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:58 +msgid "Proxy user name" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:61 +msgid "Proxy password" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:64 +msgid "Connection timeout" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:65 +msgid "" +"Connection timeout, in seconds; keep empty to use system's default, which is " +"also none by default" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:74 +msgid "Missing hostname!" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/url.py:79 +msgid "Proxy server defined without proxy port!" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:104 +#: ./src/pyams_scheduler/interfaces/zodb.py:31 +msgid "ZEO connection name" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:105 +msgid "Name of ZEO connection utility defining scheduler connection" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:109 +msgid "Reports mailer" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:110 +msgid "Mail delivery utility used to send mails" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:114 +msgid "Reports source" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:115 +msgid "Mail address from which reports will be sent" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:126 +msgid "Scheduler tasks" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:127 +msgid "List of tasks assigned to this scheduler" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:130 +#: ./src/pyams_scheduler/interfaces/__init__.py:203 +msgid "History" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:155 +msgid "Task name" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:156 +msgid "Descriptive name given to this task" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:159 +msgid "Scheduling mode" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:160 +msgid "Scheduling mode defines how task will be scheduled" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:164 +msgid "Reports target(s)" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:165 +msgid "" +"Mail address(es) to which execution reports will be sent; you can enter " +"several addresses separated by semicolons" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:169 +msgid "Errors reports target(s)" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:170 +msgid "" +"Mail address(es) to which error reports will be sent; you can enter several " +"addresses separated by semicolons; keep empty to use normal reports target" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:175 +msgid "Only report errors?" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:176 +msgid "If 'Yes', only error reports will be sent to given errors target" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:180 +msgid "Send empty reports?" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:181 +msgid "If 'No', empty reports will not be sent by mail" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:185 +msgid "Keep empty reports history?" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:186 +msgid "If 'Yes', empty reports will be kept in task history" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:190 +msgid "History duration" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:191 +msgid "" +"Number of days during which execution reports are kept in history; enter 0 to" +" remove limit" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:195 +msgid "History max length" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:196 +msgid "Number of execution reports to keep in history; enter 0 to remove limit" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:258 +msgid "Active task" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:259 +msgid "You can disable a task by selecting 'No'" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:263 +msgid "First execution date" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:264 +msgid "Date from which scheduling should start" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:280 +#: ./src/pyams_scheduler/interfaces/__init__.py:354 +msgid "Last execution date" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:281 +#: ./src/pyams_scheduler/interfaces/__init__.py:355 +msgid "Date past which scheduling should end" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:284 +msgid "Years" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:285 +msgid "Years for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:289 +msgid "Months" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:290 +msgid "Months (1-12) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:294 +msgid "Month days" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:295 +msgid "Days (1-31) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:299 +msgid "Weeks" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:300 +msgid "Year weeks (1-53) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:304 +msgid "Week days" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:305 +msgid "Week days (0-6, with 0 as monday) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:309 +msgid "Hours" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:310 +msgid "Hours (0-23) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:314 +msgid "Minutes" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:315 +msgid "Minutes (0-59) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:319 +msgid "Seconds" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:320 +msgid "Seconds (0-59) for which to schedule the job" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:337 +msgid "Execution date" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:338 +msgid "Date on which execution should start" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:358 +msgid "Weeks interval" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:359 +msgid "Number of weeks between executions" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:363 +msgid "Days interval" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:364 +msgid "Number of days between executions" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:368 +msgid "Hours interval" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:369 +msgid "Number of hours between executions" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:373 +msgid "Minutes interval" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:374 +msgid "Number of minutes between executions" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:378 +msgid "Seconds interval" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/__init__.py:379 +msgid "Number of seconds between executions" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:31 +msgid "Target hostname of IP address" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:32 +msgid "" +"Enter hostname or address of a remote hots; keep empty for local server host" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:35 +msgid "SSH port number" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:42 +msgid "Private key filename" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:43 +msgid "" +"Enter name of private key file; use '~' to identify running server user home " +"directory, as in ~/.ssh/id_rsa" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:48 +msgid "If not using private key, you must provide user's password" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:51 +msgid "Command line" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:52 +msgid "Enter command line, using absolute path names" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/ssh.py:58 +msgid "" +"You must provide a private key filename OR a password when defining remote " +"tasks" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/zodb.py:32 +msgid "Name of ZEO connection utility pointing to packed database" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/zodb.py:36 +msgid "Maximum transactions age" +msgstr "" + +#: ./src/pyams_scheduler/interfaces/zodb.py:37 +msgid "Transactions older than this age, in days, will be removed" +msgstr "" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/process.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/process.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,231 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import logging +logger = logging.getLogger('PyAMS (scheduler)') + +from datetime import datetime +from threading import Thread + +# import interfaces +from pyams_scheduler.interfaces import SCHEDULER_NAME +from pyams_utils.interfaces import PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME +from pyams_utils.interfaces.zeo import IZEOConnection + +# import packages +from apscheduler.jobstores.memory import MemoryJobStore +from apscheduler.schedulers.background import BackgroundScheduler +from pyams_scheduler.task import ImmediateTaskTrigger +from pyams_utils.zodb import ZEOConnection +from pyams_zmq.handler import ZMQMessageHandler +from pyams_zmq.process import ZMQProcess + + +class BaseTaskThread(Thread): + + def __init__(self, process, settings): + Thread.__init__(self) + self.process = process + self.settings = settings + + def _get_connection(self): + zeo_settings = self.settings.get('zeo') + connection = ZEOConnection() + connection.update(zeo_settings) + return connection + + +class TaskResettingThread(BaseTaskThread): + """Task resetting thread + + Task reset is run in another thread, so that: + - other transactions applied on updated tasks are visible + - ØMQ request returns immediately to calling process + """ + + def run(self): + logger.debug("Starting task resetting thread...") + settings = self.settings + job_id = settings.get('job_id') + if job_id is None: + return + job_id = str(job_id) + logger.debug("Loading ZEO connection...") + with self._get_connection() as root: + logger.debug("Loaded ZODB root {0!r}".format(root)) + tm = None + try: + try: + application_name = self.process.registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY, + PYAMS_APPLICATION_DEFAULT_NAME) + application = root.get(application_name) + logger.debug("Loaded application {0!r}".format(application)) + sm = application.getSiteManager() + scheduler_util = sm.get(SCHEDULER_NAME) + logger.debug("Loaded scheduler utility {0!r}".format(scheduler_util)) + scheduler = self.process.scheduler + logger.debug("Removing job '{0}'".format(job_id)) + job = scheduler.get_job(job_id) + if job is not None: + logger.debug("Loaded job {0!r} ({0.id!r})".format(job)) + scheduler.remove_job(job.id) + logger.debug("Loading scheduler task '{0}'".format(settings.get('task_name').lower())) + task = scheduler_util.get(settings.get('task_name').lower()) + logger.debug("Loaded scheduler task {0!r}".format(task)) + if task is not None: + trigger = task.get_trigger(self.process.registry) + logger.debug("Getting task trigger {0!r}".format(trigger)) + zeo_connection = sm.getUtility(IZEOConnection, name=scheduler_util.zeo_connection) + logger.debug("Adding new job to scheduler {0!r}".format(scheduler)) + scheduler.add_job(task, trigger, + id=str(task.internal_id), + name=task.name, + kwargs={'zeo_settings': zeo_connection.get_settings(), + 'registry': self.process.registry}) + logger.debug("Added job") + except: + logger.exception("An exception occurred:") + finally: + if tm is not None: + tm.abort() + + +class TaskRemoverThread(BaseTaskThread): + """Task remover thread""" + + def run(self): + logger.debug("Starting task remover thread...") + settings = self.settings + job_id = settings.get('job_id') + if job_id is None: + return + job_id = str(job_id) + logger.debug("Loading ZEO connection...") + with self._get_connection() as root: + logger.debug("Loaded ZODB root {0!r}".format(root)) + tm = None + try: + try: + application_name = self.process.registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY, + PYAMS_APPLICATION_DEFAULT_NAME) + application = root.get(application_name) + logger.debug("Loaded application {0!r}".format(application)) + sm = application.getSiteManager() + scheduler_util = sm.get(SCHEDULER_NAME) + logger.debug("Loaded scheduler utility {0!r}".format(scheduler_util)) + scheduler = self.process.scheduler + logger.debug("Removing job '{0}'".format(job_id)) + job = scheduler.get_job(job_id) + if job is not None: + logger.debug("Loaded job {0!r} ({0.id!r})".format(job)) + scheduler.remove_job(job.id) + logger.debug("Removed job") + except: + logger.exception("An exception occurred:") + finally: + if tm is not None: + tm.abort() + + +class TaskRunnerThread(BaseTaskThread): + """Task immediate runner thread""" + + def run(self): + logger.debug("Starting task runner thread...") + settings = self.settings + job_id = settings.get('job_id') + if job_id is None: + return + logger.debug("Loading ZEO connection...") + with self._get_connection() as root: + logger.debug("Loaded ZODB root {0!r}".format(root)) + tm = None + try: + try: + application_name = self.process.registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY, + PYAMS_APPLICATION_DEFAULT_NAME) + application = root.get(application_name) + logger.debug("Loaded application {0!r}".format(application)) + sm = application.getSiteManager() + scheduler_util = sm.get(SCHEDULER_NAME) + logger.debug("Loaded scheduler utility {0!r}".format(scheduler_util)) + scheduler = self.process.scheduler + logger.debug("Loading scheduler task '{0}'".format(settings.get('task_name').lower())) + task = scheduler_util.get(settings.get('task_name').lower()) + logger.debug("Loaded scheduler task {0!r}".format(task)) + if task is not None: + trigger = ImmediateTaskTrigger() + logger.debug("Getting task trigger {0!r}".format(trigger)) + zeo_connection = sm.getUtility(IZEOConnection, name=scheduler_util.zeo_connection) + logger.debug("Adding new job to scheduler {0!r}".format(scheduler)) + scheduler.add_job(task, trigger, + id='{0.internal_id}::{1}'.format(task, + datetime.utcnow().isoformat()), + name=task.name, + kwargs={'zeo_settings': zeo_connection.get_settings(), + 'registry': self.process.registry, + 'run_immediate': True}) + logger.debug("Added job") + except: + logger.exception("An exception occurred:") + finally: + if tm is not None: + tm.abort() + + +class SchedulerHandler(object): + """Scheduler handler""" + + def get_jobs(self, settings): + scheduler = self.process.scheduler + return [{'id': job.id, + 'name': job.name, + 'trigger': '{0!s}'.format(job.trigger), + 'next_run': job.next_run_time.timestamp()} for job in scheduler.get_jobs()] + + def reset_task(self, settings): + TaskResettingThread(self.process, settings).start() + return 'OK' + + def remove_task(self, settings): + TaskRemoverThread(self.process, settings).start() + return 'OK' + + def run_task(self, settings): + TaskRunnerThread(self.process, settings).start() + return 'OK' + + +class SchedulerMessageHandler(ZMQMessageHandler): + """ØMQ scheduler messages handler""" + + handler = SchedulerHandler + + +class SchedulerProcess(ZMQProcess): + """ØMQ tasks scheduler process""" + + def __init__(self, zmq_address, handler, registry): + ZMQProcess.__init__(self, zmq_address, handler) + self.registry = registry + self.scheduler = BackgroundScheduler() + self.jobstore = MemoryJobStore() + + def run(self): + if self.scheduler is not None: + self.scheduler.add_jobstore(self.jobstore, 'default') + self.scheduler.start() + ZMQProcess.run(self) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/scheduler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/scheduler.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,83 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import zmq + +# import interfaces +from pyams_scheduler.interfaces import IScheduler, ISchedulerHandler, SCHEDULER_HANDLER_KEY +from zope.intid.interfaces import IIntIds + +# import packages +from pyams_utils.registry import query_utility +from pyams_utils.request import check_request +from zope.container.folder import Folder +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(ISchedulerHandler) +class SchedulerHandler(object): + """Scheduler handler utility + + This is just a 'marker' utility which is used to mark nodes in a cluster + which should run the scheduler + """ + + +@implementer(IScheduler) +class Scheduler(Folder): + """Scheduler utility""" + + zeo_connection = FieldProperty(IScheduler['zeo_connection']) + report_mailer = FieldProperty(IScheduler['report_mailer']) + report_source = FieldProperty(IScheduler['report_source']) + + @property + def tasks(self): + return list(self.values()) + + @property + def history(self): + result = [] + [result.extend(task.history) for task in self.values()] + result.sort(key=lambda x: x.date) + return result + + @property + def internal_id(self): + intids = query_utility(IIntIds, context=self) + if intids is not None: + return intids.register(self) + + def get_task(self, task_id): + intids = query_utility(IIntIds, context=self) + if intids is not None: + return intids.queryObject(task_id) + + def _get_socket(self): + """Open ØMQ socket""" + request = check_request() + handler = request.registry.settings.get(SCHEDULER_HANDLER_KEY, False) + if handler: + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect('tcp://{0}'.format(handler)) + return socket + + def get_jobs(self): + socket = self._get_socket() + socket.send_json(['get_jobs', {}]) + return socket.recv_json() diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/site.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/site.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,48 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces import IScheduler, SCHEDULER_NAME +from pyams_utils.interfaces.site import ISiteGenerations +from zope.site.interfaces import INewLocalSite + +# import packages +from pyams_scheduler.scheduler import Scheduler +from pyams_utils.registry import utility_config +from pyams_utils.site import check_required_utilities +from pyramid.events import subscriber + + +REQUIRED_UTILITIES = ((IScheduler, '', Scheduler, SCHEDULER_NAME), ) + + +@subscriber(INewLocalSite) +def handle_new_local_site(event): + """Create a new scheduler when a site is created""" + site = event.manager.__parent__ + check_required_utilities(site, REQUIRED_UTILITIES) + + +@utility_config(name='PyAMS scheduler', provides=ISiteGenerations) +class SchedulerGenerationsChecker(object): + """Scheduler generations checker""" + + generation = 1 + + def evolve(self, site, current=None): + """Check for required utilities""" + check_required_utilities(site, REQUIRED_UTILITIES) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/ssh.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/ssh.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,74 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import os.path +import subprocess +import sys +import traceback + +# import interfaces +from pyams_scheduler.interfaces.ssh import ISSHCallerTask + +# import packages +import paramiko +from paramiko.ssh_exception import SSHException +from pyams_scheduler.task import Task +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(ISSHCallerTask) +class SSHCallerTask(Task): + """SSH caller task""" + + hostname = FieldProperty(ISSHCallerTask['hostname']) + port = FieldProperty(ISSHCallerTask['port']) + username = FieldProperty(ISSHCallerTask['username']) + private_key = FieldProperty(ISSHCallerTask['private_key']) + password = FieldProperty(ISSHCallerTask['password']) + cmdline = FieldProperty(ISSHCallerTask['cmdline']) + + def run(self, report): + if self.hostname: + self._run_remote(report) + else: + self._run_local(report) + + def _run_remote(self, report): + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(self.hostname, self.port, self.username, self.password, + key_filename=os.path.expanduser(self.private_key) if self.private_key else None) + try: + stdin, stdout, stderr = ssh.exec_command(self.cmdline) + stdin.close() + report.write(stdout.read().decode()) + errors = stderr.read() + if errors: + report.write('\n\nSome errors occurred\n===================\n') + report.write(errors.decode()) + except SSHException: + etype, value, tb = sys.exc_info() + report.write('\n\nAn error occurred\n================\n') + report.write(''.join(traceback.format_exception(etype, value, tb))) + + def _run_local(self, report): + shell = subprocess.Popen(self.cmdline, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = shell.communicate() + report.write(stdout.decode()) + if stderr: + report.write('\n\nSome errors occurred\n===================\n') + report.write(stderr.decode()) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/task.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/task.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,365 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import logging +logger = logging.getLogger('PyAMS (scheduler)') + +import traceback +from datetime import datetime, timedelta +from io import StringIO + +# import interfaces +from pyams_scheduler.interfaces import ITaskHistory, ITask, ITaskHistoryContainer, ITaskSchedulingMode, IScheduler, \ + SCHEDULER_HANDLER_KEY, AfterRunJobEvent, SCHEDULER_NAME, BeforeRunJobEvent, ITaskInfo +from pyams_utils.interfaces import PYAMS_APPLICATION_DEFAULT_NAME, PYAMS_APPLICATION_SETTINGS_KEY +from pyams_utils.interfaces.zeo import IZEOConnection +from pyramid_mailer.interfaces import IMailer +from transaction.interfaces import ITransactionManager +from zope.component.interfaces import ISite +from zope.intid.interfaces import IIntIds + +# import packages +import transaction +import zmq +from apscheduler.triggers.base import BaseTrigger +from persistent import Persistent +from pyams_utils.date import get_duration +from pyams_utils.registry import query_utility, get_utility +from pyams_utils.request import check_request +from pyams_utils.timezone import tztime +from pyams_utils.traversing import get_parent +from pyams_utils.zodb import ZEOConnection +from pyramid.events import subscriber +from pyramid_mailer.message import Message +from zope.container.contained import Contained +from zope.container.folder import Folder +from zope.interface import implementer, alsoProvides, noLongerProvides +from zope.lifecycleevent import ObjectRemovedEvent, ObjectModifiedEvent +from zope.location import locate +from zope.schema.fieldproperty import FieldProperty + + +class ImmediateTaskTrigger(BaseTrigger): + """Immediate-style task scheduler""" + + def get_next_fire_time(self, previous_fire_time, now): + if previous_fire_time: + return None + else: + return now + timedelta(seconds=5) + + +@implementer(ITaskHistory) +class TaskHistoryItem(Persistent, Contained): + """Task history item""" + + date = FieldProperty(ITaskHistory['date']) + status = FieldProperty(ITaskHistory['status']) + report = FieldProperty(ITaskHistory['report']) + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +@implementer(ITaskHistoryContainer) +class TaskHistoryContainer(Folder): + """Task history container""" + + def check_history(self, duration, length): + now = tztime(datetime.utcnow()) + if duration: + for key in [k for k in self.keys()]: + if (now - self[key].date).days > duration: + del self[key] + if length and (len(self) > length): + keys = sorted(self.keys(), reverse=True)[:length] + for key in [k for k in self.keys()]: + if key not in keys: + del self[key] + + +@implementer(ITask) +class Task(Persistent, Contained): + """Task definition persistent class""" + + name = FieldProperty(ITask['name']) + _schedule_mode = FieldProperty(ITask['schedule_mode']) + report_target = FieldProperty(ITask['report_target']) + errors_target = FieldProperty(ITask['errors_target']) + report_errors_only = FieldProperty(ITask['report_errors_only']) + send_empty_reports = FieldProperty(ITask['send_empty_reports']) + keep_empty_reports = FieldProperty(ITask['keep_empty_reports']) + _history_duration = FieldProperty(ITask['history_duration']) + _history_length = FieldProperty(ITask['history_length']) + + def __init__(self): + history = self.history = TaskHistoryContainer() + locate(history, self, '++history++') + + @property + def schedule_mode(self): + return self._schedule_mode + + @schedule_mode.setter + def schedule_mode(self, value): + if self._schedule_mode is not None: + mode = query_utility(ITaskSchedulingMode, name=self._schedule_mode) + if (mode is not None) and mode.marker_interface.providedBy(self): + noLongerProvides(self, mode.marker_interface) + self._schedule_mode = value + if value: + mode = get_utility(ITaskSchedulingMode, name=value) + alsoProvides(self, mode.marker_interface) + mode.schema(self).active = False + self.reset() + + @property + def history_duration(self): + return self._history_duration + + @history_duration.setter + def history_duration(self, value): + self._history_duration = value + + @property + def history_length(self): + return self._history_length + + @history_length.setter + def history_length(self, value): + self._history_length = value + + def check_history(self): + self.history.check_history(self.history_duration, self.history_length) + + @property + def internal_id(self): + site = get_parent(self, ISite) + sm = site.getSiteManager() + intids = sm.queryUtility(IIntIds) + if intids is not None: + return intids.register(self) + + def get_trigger(self, registry): + mode = registry.queryUtility(ITaskSchedulingMode, self.schedule_mode) + if mode is None: + return None + return mode.get_trigger(self) + + def get_scheduling_info(self, registry): + mode = registry.queryUtility(ITaskSchedulingMode, self.schedule_mode) + if mode is None: + return None + return mode.schema(self, None) + + def reset(self): + scheduler_util = query_utility(IScheduler) + if scheduler_util is not None: + request = check_request() + transaction.get().addAfterCommitHook(self._reset_action, kws={'scheduler': scheduler_util, + 'registry': request.registry}) + + def _reset_action(self, status, *args, **kwargs): + if not status: + return + scheduler_util = kwargs.get('scheduler') + if scheduler_util is None: + return + request = check_request() + if request.registry: + handler = request.registry.settings.get(SCHEDULER_HANDLER_KEY, False) + if handler: + zeo = get_utility(IZEOConnection, scheduler_util.zeo_connection) + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect('tcp://{0}'.format(handler)) + zmq_settings = {'zeo': zeo.get_settings(), + 'task_name': self.__name__, + 'job_id': self.internal_id} + logger.debug("Resetting task {0.name} with {1!r}".format(self, zmq_settings)) + socket.send_json(['reset_task', zmq_settings]) + socket.recv_json() + + def launch(self): + scheduler_util = query_utility(IScheduler) + if scheduler_util is not None: + transaction.get().addAfterCommitHook(self._launch_action, kws={'scheduler': scheduler_util}) + + def _launch_action(self, status, *args, **kwargs): + if not status: + return + scheduler_util = kwargs.get('scheduler') + if scheduler_util is None: + return + request = check_request() + if request.registry: + handler = request.registry.settings.get(SCHEDULER_HANDLER_KEY, False) + if handler: + zeo = get_utility(IZEOConnection, scheduler_util.zeo_connection) + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect('tcp://{0}'.format(handler)) + zmq_settings = {'zeo': zeo.get_settings(), + 'task_name': self.__name__, + 'job_id': self.internal_id} + logger.debug("Running task {0.name} with {1!r}".format(self, zmq_settings)) + socket.send_json(['run_task', zmq_settings]) + socket.recv_json() + + def __call__(self, *args, **kwargs): + report = StringIO() + self._run(report, **kwargs) + + def is_runnable(self, registry): + mode = registry.queryUtility(ITaskSchedulingMode, self.schedule_mode) + if mode is None: + return False + info = mode.schema(self, None) + if info is None: + return False + return info.active + + def _run(self, report, **kwargs): + """Task execution wrapper""" + zeo_connection = ZEOConnection() + zeo_connection.update(kwargs.get('zeo_settings')) + with zeo_connection as root: + try: + registry = kwargs.get('registry') + request = check_request() + request.registry = registry + application_name = registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY, + PYAMS_APPLICATION_DEFAULT_NAME) + sm = root.get(application_name).getSiteManager() + scheduler_util = sm.get(SCHEDULER_NAME) + task = scheduler_util.get(self.__name__) + if task is not None: + if not (kwargs.get('run_immediate') or task.is_runnable(registry)): + logger.debug("Skipping inactive task {0}".format(task.name)) + return + tm = ITransactionManager(task) + for attempt in tm.attempts(): + with attempt as t: + start = datetime.utcnow() + try: + registry.notify(BeforeRunJobEvent(task)) + task.run(report) + if report.getvalue(): + status = 'OK' + else: + status = 'Empty' + report.write('\n\nTask duration: {0}'.format(get_duration(start, request=request))) + except: + status = 'Error' + task._log_exception(report, + "An error occurred during execution of task '{0}'".format(task.name)) + registry.notify(AfterRunJobEvent(task, status)) + task.store_report(report, status) + task.send_report(report, status, registry) + if t.status == 'Committed': + break + except: + self._log_exception(None, "Can't execute scheduled job {0}".format(self.name)) + ITransactionManager(self).abort() + + def run(self, report): + raise NotImplemented("The 'run' method must be implemented by Task subclasses!") + + @staticmethod + def _log_report(report, message, add_timestamp=True, level=logging.INFO): + if isinstance(message, bytes): + message = message.decode() + if add_timestamp: + message = '{0} - {1}'.format(tztime(datetime.utcnow()).strftime('%c'), message) + if report is not None: + report.write(message + '\n') + logger.log(level, message) + + @staticmethod + def _log_exception(report, message=None): + if isinstance(message, bytes): + message = message.decode() + message = '{0} - {1}'.format(tztime(datetime.utcnow()).strftime('%c'), message or 'An error occurred') + '\n\n' + if report is not None: + report.write(message) + report.write(traceback.format_exc() + '\n') + logger.exception(message) + + def store_report(self, report, status): + if (status == 'Empty') and not self.keep_empty_reports: + return + item = TaskHistoryItem(date=tztime(datetime.utcnow()), + status=status, + report=report.getvalue()) + self.history[item.date.isoformat()] = item + self.check_history() + + def send_report(self, report, status, registry): + if not self.__parent__.report_mailer: + return + if ((status == 'Empty') and not self.send_empty_reports) or \ + ((status == 'OK') and self.report_errors_only): + return + message_target = self.report_target + if status in ('Error', 'Warning'): + message_target = self.errors_target or message_target + if not message_target: + return + mailer = registry.queryUtility(IMailer, self.__parent__.report_mailer) + if mailer is not None: + report_source = self.__parent__.report_source + if status == 'Error': + subject = "[SCHEDULER ERROR] {0}".format(self.name) + else: + subject = "[scheduler] {0}".format(self.name) + for target in message_target.split(';'): + message = Message(subject=subject, + sender=report_source, + recipients=(target,), + body=report.getvalue()) + mailer.send(message) + + +@subscriber(ObjectModifiedEvent, context_selector=ITask) +def handle_modified_task(event): + """Handle modified task""" + for changes in event.descriptions: + if (changes.interface == ITaskInfo) and \ + (('history_duration' in changes.attributes) or ('history_length' in changes.attributes)): + event.object.check_history() + break + + +@subscriber(ObjectRemovedEvent, context_selector=ITask) +def handle_removed_task(event): + """Handle removed task""" + request = check_request() + if request.registry: + handler = request.registry.settings.get(SCHEDULER_HANDLER_KEY, False) + if handler: + task = event.object + scheduler_util = query_utility(IScheduler) + zeo = get_utility(IZEOConnection, scheduler_util.zeo_connection) + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect('tcp://{0}'.format(handler)) + zmq_settings = {'zeo': zeo.get_settings(), + 'task_name': task.__name__, + 'job_id': task.internal_id} + logger.debug("Removing task {0.name} with {1!r}".format(task, zmq_settings)) + socket.send_json(['remove_task', zmq_settings]) + socket.recv_json() diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/tests/__init__.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/tests/test_utilsdocs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/tests/test_utilsdocs.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,59 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +Generic Test case for pyams_scheduler doctest +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.dirname(__file__) + +def doc_suite(test_dir, setUp=None, tearDown=None, globs=None): + """Returns a test suite, based on doctests found in /doctest.""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + doctest_dir = os.path.join(package_dir, 'doctests') + + # filtering files on extension + docs = [os.path.join(doctest_dir, doc) for doc in + os.listdir(doctest_dir) if doc.endswith('.txt')] + + for test in docs: + suite.append(doctest.DocFileSuite(test, optionflags=flags, + globs=globs, setUp=setUp, + tearDown=tearDown, + module_relative=False)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/tests/test_utilsdocstrings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/tests/test_utilsdocstrings.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,62 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +Generic Test case for pyams_scheduler doc strings +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.abspath(os.path.dirname(__file__)) + +def doc_suite(test_dir, globs=None): + """Returns a test suite, based on doc tests strings found in /*.py""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + # filtering files on extension + docs = [doc for doc in + os.listdir(package_dir) if doc.endswith('.py')] + docs = [doc for doc in docs if not doc.startswith('__')] + + for test in docs: + fd = open(os.path.join(package_dir, test)) + content = fd.read() + fd.close() + if '>>> ' not in content: + continue + test = test.replace('.py', '') + location = 'pyams_scheduler.%s' % test + suite.append(doctest.DocTestSuite(location, optionflags=flags, + globs=globs)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/trigger.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/trigger.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,186 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces import ITaskSchedulingMode, ICronTaskScheduling, ICronTask, SCHEDULER_TASK_CRON_INFO, \ + IDateTaskScheduling, IDateTask, ILoopTaskScheduling, SCHEDULER_TASK_DATE_INFO, ILoopTask, SCHEDULER_TASK_LOOP_INFO +from zope.annotation.interfaces import IAnnotations +from zope.schema.interfaces import IVocabularyFactory + +# import packages +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.date import DateTrigger +from apscheduler.triggers.interval import IntervalTrigger +from persistent import Persistent +from pyams_utils.adapter import adapter_config +from pyams_utils.date import date_to_datetime +from pyams_utils.registry import utility_config +from pyams_utils.timezone import tztime +from zope.componentvocabulary.vocabulary import UtilityVocabulary +from zope.interface import implementer, provider +from zope.schema.fieldproperty import FieldProperty +from zope.schema.vocabulary import getVocabularyRegistry + +from pyams_scheduler import _ + + +@provider(IVocabularyFactory) +class SchedulingModesVocabulary(UtilityVocabulary): + """Scheduling modes vocabulary""" + + interface = ITaskSchedulingMode + nameOnly = True + +getVocabularyRegistry().register('PyAMS scheduling modes', SchedulingModesVocabulary) + + +# +# Cron-style scheduling mode +# + +@implementer(ICronTaskScheduling) +class CronTaskScheduleInfo(Persistent): + """Cron-style schedule info""" + + active = FieldProperty(ICronTaskScheduling['active']) + start_date = FieldProperty(ICronTaskScheduling['start_date']) + end_date = FieldProperty(ICronTaskScheduling['end_date']) + year = FieldProperty(ICronTaskScheduling['year']) + month = FieldProperty(ICronTaskScheduling['month']) + day = FieldProperty(ICronTaskScheduling['day']) + week = FieldProperty(ICronTaskScheduling['week']) + day_of_week = FieldProperty(ICronTaskScheduling['day_of_week']) + hour = FieldProperty(ICronTaskScheduling['hour']) + minute = FieldProperty(ICronTaskScheduling['minute']) + second = FieldProperty(ICronTaskScheduling['second']) + + +@utility_config(name='Cron-style scheduling', provides=ITaskSchedulingMode) +class CronTaskScheduler(object): + """Cron-style scheduler mode""" + + marker_interface = ICronTask + schema = ICronTaskScheduling + + def get_trigger(self, task): + if not self.marker_interface.providedBy(task): + raise Exception(_("Task is not configured for cron-style scheduling!")) + info = self.schema(task) + return CronTrigger(year=info.year or '*', + month=info.month or '*', + day=info.day or '*', + week=info.week or '*', + day_of_week=info.day_of_week or '*', + hour=info.hour or '*', + minute=info.minute or '*', + second=info.second or '0', + start_date=tztime(date_to_datetime(info.start_date)), + end_date=tztime(date_to_datetime(info.end_date))) + + +@adapter_config(context=ICronTask, provides=ICronTaskScheduling) +def CronTaskSchedulerInfoFactory(context): + """Cron-style task scheduling info factory""" + annotations = IAnnotations(context) + info = annotations.get(SCHEDULER_TASK_CRON_INFO) + if info is None: + info = annotations[SCHEDULER_TASK_CRON_INFO] = CronTaskScheduleInfo() + return info + + +# +# Date-style scheduling mode +# + +@implementer(IDateTaskScheduling) +class DateTaskScheduleInfo(Persistent): + """Date-style schedule info""" + + active = FieldProperty(IDateTaskScheduling['active']) + start_date = FieldProperty(IDateTaskScheduling['start_date']) + + +@utility_config(name='Date-style scheduling', provides=ITaskSchedulingMode) +class DateTaskScheduler(object): + """Date-style scheduler mode""" + + marker_interface = IDateTask + schema = IDateTaskScheduling + + def get_trigger(self, task): + if not self.marker_interface.providedBy(task): + raise Exception(_("Task is not configured for date-style scheduling!")) + info = self.schema(task) + return DateTrigger(run_date=tztime(date_to_datetime(info.start_date))) + + +@adapter_config(context=IDateTask, provides=IDateTaskScheduling) +def DateTaskSchedulerInfoFactory(context): + """Date-style task scheduling info factory""" + annotations = IAnnotations(context) + info = annotations.get(SCHEDULER_TASK_DATE_INFO) + if info is None: + info = annotations[SCHEDULER_TASK_DATE_INFO] = DateTaskScheduleInfo() + return info + + +# +# Loop-style scheduling mode +# + +@implementer(ILoopTaskScheduling) +class LoopTaskScheduleInfo(Persistent): + """Loop-style schedule info""" + + active = FieldProperty(ILoopTaskScheduling['active']) + start_date = FieldProperty(ILoopTaskScheduling['start_date']) + end_date = FieldProperty(ILoopTaskScheduling['end_date']) + weeks = FieldProperty(ILoopTaskScheduling['weeks']) + days = FieldProperty(ILoopTaskScheduling['days']) + hours = FieldProperty(ILoopTaskScheduling['hours']) + minutes = FieldProperty(ILoopTaskScheduling['minutes']) + seconds = FieldProperty(ILoopTaskScheduling['seconds']) + + +@utility_config(name='Loop-style scheduling', provides=ITaskSchedulingMode) +class LoopTaskScheduler(object): + """Loop-style scheduler mode""" + + marker_interface = ILoopTask + schema = ILoopTaskScheduling + + def get_trigger(self, task): + if not self.marker_interface.providedBy(task): + raise Exception(_("Task is not configured for loop-style scheduling!")) + info = self.schema(task) + return IntervalTrigger(weeks=info.weeks, + days=info.days, + hours=info.hours, + minutes=info.minutes, + seconds=info.seconds, + start_date=tztime(date_to_datetime(info.start_date)), + end_date=tztime(date_to_datetime(info.end_date))) + + +@adapter_config(context=ILoopTask, provides=ILoopTaskScheduling) +def LoopTaskSchedulerInfoFactory(context): + """Loop-style task scheduling info factory""" + annotations = IAnnotations(context) + info = annotations.get(SCHEDULER_TASK_LOOP_INFO) + if info is None: + info = annotations[SCHEDULER_TASK_LOOP_INFO] = LoopTaskScheduleInfo() + return info diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/url.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/url.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,74 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import codecs +import chardet +from urllib import parse + +# import interfaces +from pyams_scheduler.interfaces.url import IURLCallerTask + +# import packages +from pyams_scheduler.task import Task +from pyams_utils.html import html_to_text +from pyams_utils.protocol.http import HTTPClient +from pyramid.exceptions import ConfigurationError +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IURLCallerTask) +class URLCallerTask(Task): + """URL caller task""" + + url = FieldProperty(IURLCallerTask['url']) + username = FieldProperty(IURLCallerTask['username']) + password = FieldProperty(IURLCallerTask['password']) + proxy_server = FieldProperty(IURLCallerTask['proxy_server']) + proxy_port = FieldProperty(IURLCallerTask['proxy_port']) + remote_dns = FieldProperty(IURLCallerTask['remote_dns']) + proxy_username = FieldProperty(IURLCallerTask['proxy_username']) + proxy_password = FieldProperty(IURLCallerTask['proxy_password']) + connection_timeout = FieldProperty(IURLCallerTask['connection_timeout']) + + def run(self, report): + parser = parse.urlparse(self.url) + if not parser.netloc: + raise ConfigurationError("Missing hostname. Task aborted.") + if self.proxy_server and not self.proxy_port: + raise ConfigurationError("Proxy server defined without proxy port. Task aborted.") + params = parser.query and dict([part.split('=') for part in parser.query.split('&')]) or {} + credentials = (self.username, self.password) if self.username else () + proxy = (self.proxy_server, self.proxy_port) if self.proxy_server else () + proxy_auth = (self.proxy_username, self.proxy_password) if self.proxy_username else () + client = HTTPClient('GET', parser.scheme, parser.netloc, parser.path, params=params, + credentials=credentials, proxy=proxy, rdns=self.remote_dns, proxy_auth=proxy_auth, + timeout=self.connection_timeout) + response, content = client.get_response() + if response.status == 200: + message = '\n'.join(['{0}={1}'.format(k, v) for k, v in sorted(response.items())]) + if isinstance(message, bytes): + message = message.decode() + report.write(message) + report.write('\n\n') + content_type = response.get('Content-Type', 'text/plain') + if content_type.startswith('text/html'): + content = html_to_text(content) + if 'charset=' in content_type.lower(): + charset = content_type.split('=', 1)[1] + else: + charset = chardet.detect(content).get('encoding') or 'utf-8' + report.write(codecs.decode(content, charset)) diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/__init__.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,20 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/interfaces.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/interfaces.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,25 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_skin.interfaces.viewlet import IMenuItem + +# import packages + + +class ISchedulerMenu(IMenuItem): + """Security manager menu interface""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/scheduler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/scheduler.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,383 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager +from pyams_template.template import template_config +from pyams_utils.date import format_datetime +from pyams_utils.interfaces.zeo import IZEOConnection + +__docformat__ = 'restructuredtext' + + +# import standard library +import logging +logger = logging.getLogger('PyAMS (scheduler)') + +from datetime import datetime + +# import interfaces +from pyams_scheduler.interfaces import IScheduler, SCHEDULER_HANDLER_KEY +from pyams_scheduler.zmi.interfaces import ISchedulerMenu +from pyams_skin.interfaces import IInnerPage, IPageHeader +from pyams_skin.interfaces.container import ITableElementEditor +from pyams_skin.layer import IPyAMSLayer +from pyams_zmi.interfaces.menu import IControlPanelMenu +from pyams_zmi.layer import IAdminLayer +from z3c.table.interfaces import IColumn, IValues +from zope.component.interfaces import ISite + +# import packages +import zmq +from pyams_form.form import AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.container import ContainerView +from pyams_skin.table import DefaultElementEditorAdapter, BaseTable, I18nColumn, TrashColumn, ActionColumn +from pyams_skin.viewlet.menu import MenuItem +from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter +from pyams_utils.registry import query_utility, get_utility +from pyams_utils.traversing import get_parent +from pyams_utils.url import absolute_url +from pyams_viewlet.manager import viewletmanager_config +from pyams_viewlet.viewlet import viewlet_config, Viewlet +from pyams_zmi.form import AdminDialogEditForm, AdminDialogDisplayForm +from pyams_zmi.view import AdminView +from pyramid.url import resource_url +from pyramid.view import view_config +from z3c.form import field +from z3c.table.column import GetAttrColumn +from zope.interface import implementer, Interface + +from pyams_scheduler import _ + + +@adapter_config(context=(IScheduler, IAdminLayer, Interface), provides=ITableElementEditor) +class SchedulerTableElementEditor(DefaultElementEditorAdapter): + """Scheduler table element editor""" + + view_name = 'scheduler-tasks.html' + modal_target = False + + @property + def url(self): + site = get_parent(self.context, ISite) + return resource_url(site, self.request, 'admin.html#{0}'.format(self.view_name)) + + +@viewlet_config(name='scheduler.menu', context=ISite, layer=IAdminLayer, manager=IControlPanelMenu, + permission='system.view', weight=10) +@viewletmanager_config(name='scheduler.menu', context=ISite, layer=IAdminLayer) +@implementer(ISchedulerMenu) +class SchedulerMenuItem(MenuItem): + """Scheduler menu""" + + label = _("Tasks scheduler") + icon_class = 'fa fa-fw fa-clock-o' + url = '#scheduler-tasks.html' + + +class SchedulerTasksTable(BaseTable): + """Scheduler tasks table""" + + id = 'scheduler_tasks_table' + title = _("Scheduled tasks") + cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight datatable'} + + @property + def data_attributes(self): + manager = query_utility(IScheduler) + attributes = super(SchedulerTasksTable, self).data_attributes + table_attrs = {'data-ams-location': absolute_url(manager, self.request), + 'data-ams-delete-target': 'delete-task.json'} + if 'table' in attributes: + attributes['table'].update(table_attrs) + else: + attributes['table'] = table_attrs + return attributes + + +@adapter_config(name='name', context=(Interface, IAdminLayer, SchedulerTasksTable), provides=IColumn) +class SchedulerTasksNameColumn(I18nColumn, GetAttrColumn): + """Scheduler tasks name column""" + + _header = _("Name") + attrName = 'name' + weight = 10 + + +@adapter_config(name='settings', context=(Interface, IAdminLayer, SchedulerTasksTable), provides=IColumn) +class SchedulerTasksSettingsColumn(ActionColumn): + """Scheduler tasks settings column""" + + icon_class = 'fa fa-fw fa-edit' + icon_hint = _("Task settings") + url = 'settings.html' + target = None + modal_target = True + + permission = 'system.view' + weight = 1 + + +@adapter_config(name='scheduler', context=(Interface, IAdminLayer, SchedulerTasksTable), provides=IColumn) +class SchedulerTasksScheduleColumn(ActionColumn): + """Scheduler tasks schedule column""" + + icon_class = 'fa fa-fw fa-calendar' + icon_hint = _("Schedule task") + url = 'schedule.html' + target = None + modal_target = True + + permission = 'system.view' + weight = 2 + + +@adapter_config(name='run', context=(Interface, IAdminLayer, SchedulerTasksTable), provides=IColumn) +class SchedulerTasksRunColumn(ActionColumn): + """Scheduler tasks run column""" + + icon_class = 'fa fa-fw fa-play' + icon_hint = _("Run task") + url = 'run.html' + target = None + modal_target = True + + permission = 'system.manage' + weight = 20 + + +@adapter_config(name='history', context=(Interface, IAdminLayer, SchedulerTasksTable), provides=IColumn) +class SchedulerTasksHistoryColumn(ActionColumn): + """Scheduler tasks history column""" + + icon_class = 'fa fa-fw fa-history' + icon_hint = _("Task run history") + url = 'history.html' + target = None + modal_target = True + + permission = 'system.view' + weight = 30 + + +@adapter_config(name='trash', context=(Interface, IAdminLayer, SchedulerTasksTable), provides=IColumn) +class SchedulerTasksTrashColumn(TrashColumn): + """Scheduler tasks trash column""" + + icon_hint = _("Delete task") + permission = 'system.manage' + + +@view_config(name='delete-task.json', context=IScheduler, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +def delete_scheduler_task(request): + """Delete task from scheduler utility""" + translate = request.localizer.translate + name = request.params.get('object_name') + if not name: + return {'status': 'message', + 'messagebox': {'status': 'error', + 'content': translate(_("No provided object_name argument!"))}} + if name not in request.context: + return {'status': 'message', + 'messagebox': {'status': 'error', + 'content': translate(_("Given task name doesn't exist!"))}} + del request.context[name] + return {'status': 'success'} + + +@adapter_config(context=(ISite, IAdminLayer, SchedulerTasksTable), provides=IValues) +class SchedulerTasksValuesAdapter(ContextRequestViewAdapter): + """Scheduler tasks values adapter""" + + @property + def values(self): + manager = query_utility(IScheduler) + if manager is not None: + return list(manager.values()) + return () + + +@pagelet_config(name='scheduler-tasks.html', context=ISite, layer=IPyAMSLayer, permission='system.view') +@implementer(IInnerPage) +class SchedulerTasksView(AdminView, ContainerView): + """Scheduler tasks view""" + + table_class = SchedulerTasksTable + + def __init__(self, context, request): + super(SchedulerTasksView, self).__init__(context, request) + + +@adapter_config(context=(ISite, IAdminLayer, SchedulerTasksView), provides=IPageHeader) +class SchedulerTasksHeaderAdapter(ContextRequestViewAdapter): + """Scheduler tasks header adapter""" + + icon_class = 'fa fa-fw fa-clock-o' + title = _("Tasks scheduler") + subtitle = _("Scheduled tasks") + + +@viewlet_config(name='scheduler.properties.menu', context=ISite, layer=IAdminLayer, + manager=ISchedulerMenu, permission='system.view', weight=1) +class SchedulerPropertiesMenuItem(MenuItem): + """Scheduler properties menu""" + + label = _("Properties...") + url = 'properties.html' + modal_target = True + + def get_url(self): + manager = query_utility(IScheduler) + return resource_url(manager, self.request, self.url) + + +@pagelet_config(name='properties.html', context=IScheduler, layer=IPyAMSLayer, permission='system.view') +class SchedulerPropertiesEditForm(AdminDialogEditForm): + """Scheduler properties edit form""" + + @property + def title(self): + return self.context.__name__ + + legend = _("Update tasks scheduler properties") + + fields = field.Fields(IScheduler).omit('__parent__', '__name__', 'tasks', 'history') + ajax_handler = 'properties.json' + edit_permission = 'system.manage' + + +@view_config(name='properties.json', context=IScheduler, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class SchedulerPropertiesAJAXEditForm(AJAXEditForm, SchedulerPropertiesEditForm): + """Scheduler properties edit form, AJAX view""" + + +@viewlet_config(name='scheduler.jobs.menu', context=ISite, layer=IAdminLayer, + manager=ISchedulerMenu, permission='system.view', weight=5) +class SchedulerJobsMenuItem(MenuItem): + """Scheduler jobs menu""" + + label = _("Active jobs...") + url = 'jobs.html' + modal_target = True + + def get_url(self): + manager = query_utility(IScheduler) + return resource_url(manager, self.request, self.url) + + +@pagelet_config(name='jobs.html', context=IScheduler, layer=IPyAMSLayer, permission='system.view') +class SchedulerJobsDisplayForm(AdminDialogDisplayForm): + """Scheduler jobs display form""" + + @property + def title(self): + return self.context.__name__ + + dialog_class = 'modal-large' + legend = _("Display scheduler active jobs") + + fields = field.Fields(Interface) + + +class SchedulerJobsTable(BaseTable): + """Scheduler jobs table""" + + title = _("Scheduler jobs") + cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight datatable'} + sortOn = None + + @property + def data_attributes(self): + return {'table': {'data-ams-datatable-global-filter': 'false', + 'data-ams-datatable-info': 'false', + 'data-ams-datatable-sdom': "t<'dt-row dt-bottom-row'<'text-right'p>>", + 'data-ams-datatable-display-length': '20', + 'data-ams-datatable-pagination-type': 'bootstrap_prevnext'}} + + +@adapter_config(name='name', context=(Interface, IAdminLayer, SchedulerJobsTable), provides=IColumn) +class SchedulerJobNameColumn(I18nColumn, GetAttrColumn): + """Scheduler job name column""" + + _header = _("Job name") + weight = 1 + + def getValue(self, obj): + return obj['name'] + + +@adapter_config(name='id', context=(Interface, IAdminLayer, SchedulerJobsTable), provides=IColumn) +class SchedulerJobIdColumn(I18nColumn, GetAttrColumn): + """Scheduler job ID column""" + + _header = _("ID") + weight = 10 + + def getValue(self, obj): + return obj['id'] + + +@adapter_config(name='trigger', context=(Interface, IAdminLayer, SchedulerJobsTable), provides=IColumn) +class SchedulerJobTriggerColumn(I18nColumn, GetAttrColumn): + """Scheduler job trigger column""" + + _header = _("Trigger") + weight = 20 + + def getValue(self, obj): + return obj['trigger'] + + +@adapter_config(name='next_run', context=(Interface, IAdminLayer, SchedulerJobsTable), provides=IColumn) +class SchedulerJobNextRunColumn(I18nColumn, GetAttrColumn): + """Scheduler job next run column""" + + _header = _("Next run") + weight = 30 + + def getValue(self, obj): + return format_datetime(datetime.utcfromtimestamp(obj['next_run']), request=self.request) + + +@adapter_config(context=(IScheduler, IAdminLayer, SchedulerJobsTable), provides=IValues) +class SchedulerJobsValuesAdapter(ContextRequestViewAdapter): + """Scheduler jobs values adapter""" + + @property + def values(self): + handler = self.request.registry.settings.get(SCHEDULER_HANDLER_KEY, False) + if handler: + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect('tcp://{0}'.format(handler)) + socket.send_json(['get_jobs', {}]) + return socket.recv_json() + else: + return () + + +@viewlet_config(name='scheduler-jobs', view=SchedulerJobsDisplayForm, layer=IAdminLayer, + manager=IWidgetsSuffixViewletsManager) +@template_config(template='templates/scheduler-jobs.pt') +class SchedulerJobsViewlet(Viewlet): + """Scheduler jobs viewlet""" + + table = SchedulerJobsTable + + def __init__(self, context, request, view, manager): + super(SchedulerJobsViewlet, self).__init__(context, request, view, manager) + self.table = SchedulerJobsTable(context, request) + + def update(self): + super(SchedulerJobsViewlet, self).update() + self.table.update() diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/ssh.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/ssh.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,91 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces.ssh import ISSHCallerTask, ISSHCallerTaskInfo +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from zope.component.interfaces import ISite + +# import packages +from pyams_form.form import AJAXAddForm, AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_scheduler.ssh import SSHCallerTask +from pyams_scheduler.zmi.scheduler import SchedulerTasksTable +from pyams_scheduler.zmi.task import TaskBaseAddForm +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogEditForm +from pyramid.view import view_config +from z3c.form import field + +from pyams_scheduler import _ + + +@viewlet_config(name='add-scheduler-ssh-task.menu', context=ISite, layer=IPyAMSLayer, + view=SchedulerTasksTable, manager=IToolbarAddingMenu, + permission='system.manage', weight=5) +class SSHTaskAddMenu(ToolbarMenuItem): + """SSH caller task add menu""" + + label = _("Add SSH command...") + label_css_class = 'fa fa-fw fa-key' + url = 'add-scheduler-ssh-task.html' + modal_target = True + + +@pagelet_config(name='add-scheduler-ssh-task.html', context=ISite, layer=IPyAMSLayer, + permission='system.manage') +class SSHTaskAddForm(TaskBaseAddForm): + """SSH command task add form""" + + legend = _("Add SSH command task") + icon_css_class = 'fa fa-fw fa-key' + + ajax_handler = 'add-scheduler-ssh-task.json' + task_factory = SSHCallerTask + + +@view_config(name='add-scheduler-ssh-task.json', context=ISite, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class SSHTaskAJAXAddForm(AJAXAddForm, SSHTaskAddForm): + """SSH command task add form, AJAX view""" + + +@pagelet_config(name='settings.html', context=ISSHCallerTask, layer=IPyAMSLayer, permission='system.view') +class SSHTaskEditForm(AdminDialogEditForm): + """SSH command task edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + legend = _("Edit task settings") + icon_css_class = 'fa fa-fw fa-key' + label_css_class = 'control-label col-md-4' + input_css_class = 'col-md-8' + + fields = field.Fields(ISSHCallerTaskInfo) + ajax_handler = 'settings.json' + edit_permission = 'system.manage' + + +@view_config(name='settings.json', context=ISSHCallerTask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class SSHTaskAJAXEditForm(AJAXEditForm, SSHTaskEditForm): + """SSH command task edit form, AJAX view""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/task.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/task.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,362 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + +# import standard library +from io import StringIO + +# import interfaces +from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager, IWidgetsPrefixViewletsManager +from pyams_scheduler.interfaces import ITaskInfo, IScheduler, ITask, ICronTask, ICronTaskScheduling, IDateTask, \ + IDateTaskScheduling, ILoopTask, ILoopTaskScheduling, ITaskHistory +from pyams_skin.interfaces import IContentHelp +from pyams_skin.layer import IPyAMSLayer +from pyams_zmi.layer import IAdminLayer +from z3c.form.interfaces import IDataExtractedEvent, DISPLAY_MODE +from z3c.table.interfaces import IValues, IColumn +from zope.traversing.interfaces import ITraversable + +# import packages +from pyams_form.form import AJAXEditForm +from pyams_form.schema import CloseButton +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.help import ContentHelp +from pyams_skin.table import BaseTable, I18nColumn +from pyams_template.template import template_config +from pyams_utils.adapter import adapter_config, ContextAdapter, ContextRequestViewAdapter +from pyams_utils.date import format_datetime +from pyams_utils.registry import query_utility +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config, Viewlet +from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm, AdminDialogDisplayForm +from pyramid.events import subscriber +from pyramid.url import resource_url +from pyramid.view import view_config +from z3c.form import field, button +from z3c.table.column import GetAttrColumn +from zope.interface import Invalid, Interface + +from pyams_scheduler import _ + + +@adapter_config(name='history', context=ITask, provides=ITraversable) +class TaskHistoryTraverser(ContextAdapter): + """Task ++history++ traverser""" + + def traverse(self, name, furtherpath=None): + return self.context.history + + +class TaskBaseAddForm(AdminDialogAddForm): + """Scheduler task base add form""" + + title = _("Tasks scheduler") + legend = _("Add URL caller task") + label_css_class = 'control-label col-md-4' + input_css_class = 'col-md-8' + + fields = field.Fields(ITaskInfo).omit('__parent__', '__name__') + edit_permission = 'system.manage' + task_factory = None + + def updateWidgets(self, prefix=None): + super(TaskBaseAddForm, self).updateWidgets() + self.widgets['history_duration'].value = 100 + self.widgets['history_length'].value = 100 + + def create(self, data): + return self.task_factory() + + def add(self, task): + context = query_utility(IScheduler) + context[task.name.lower()] = task + + def nextURL(self): + return resource_url(self.context, self.request, 'scheduler-tasks.html') + + +@subscriber(IDataExtractedEvent, form_selector=TaskBaseAddForm) +def handle_new_task_data_extraction(event): + """Handle new task form data extraction""" + manager = query_utility(IScheduler) + name = event.data.get('name') + if name.lower() in manager: + event.form.widgets.errors += (Invalid(_("Specified task name is already used!")),) + + +@pagelet_config(name='properties.html', context=ITask, layer=IPyAMSLayer, permission='system.view') +class TaskPropertiesEditForm(AdminDialogEditForm): + """Scheduler task properties edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + legend = _("Edit task properties") + icon_css_class = 'fa fa-fw fa-clock-o' + label_css_class = 'control-label col-md-4' + input_css_class = 'col-md-8' + + fields = field.Fields(ITaskInfo).omit('__parent__', '__name__') + ajax_handler = 'properties.json' + edit_permission = 'system.manage' + + def updateWidgets(self, prefix=None): + super(TaskPropertiesEditForm, self).updateWidgets(prefix) + self.widgets['name'].mode = DISPLAY_MODE + + +@view_config(name='properties.json', context=ITask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class SchedulerTaskPropertiesAJAXEditForm(AJAXEditForm, TaskPropertiesEditForm): + """Scheduler task properties edit form, AJAX view""" + + +class TaskScheduleEditForm(AdminDialogEditForm): + """Scheduler task base schedule edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + legend = _("Schedule task") + icon_css_class = 'fa fa-fw fa-calendar' + label_css_class = 'control-label col-md-4' + input_css_class = 'col-md-8' + + ajax_handler = 'schedule.json' + edit_permission = 'system.manage' + + def update_content(self, content, data): + changes = super(TaskScheduleEditForm, self).update_content(content, data) + if changes: + self.context.reset() + return changes + + +@pagelet_config(name='schedule.html', context=ICronTask, layer=IPyAMSLayer, permission='system.view') +class CronTaskScheduleEditForm(TaskScheduleEditForm): + """Cron-style task schedule edit form""" + + fields = field.Fields(ICronTaskScheduling) + + +@view_config(name='schedule.json', context=ICronTask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class CronTaskScheduleAJAXEditForm(AJAXEditForm, CronTaskScheduleEditForm): + """Cron-style task schedule edit form, AJAX view""" + + +@pagelet_config(name='schedule.html', context=IDateTask, layer=IPyAMSLayer, permission='system.view') +class DateTaskScheduleEditForm(TaskScheduleEditForm): + """Date-style task schedule edit form""" + + fields = field.Fields(IDateTaskScheduling) + + +@view_config(name='schedule.json', context=IDateTask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class DateTaskScheduleAJAXEditForm(AJAXEditForm, DateTaskScheduleEditForm): + """Date-style task schedule edit form, AJAX view""" + + +@pagelet_config(name='schedule.html', context=ILoopTask, layer=IPyAMSLayer, permission='system.view') +class LoopTaskScheduleEditForm(TaskScheduleEditForm): + """Loop-style task schedule edit form""" + + fields = field.Fields(ILoopTaskScheduling) + + +@view_config(name='schedule.json', context=ILoopTask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class LoopTaskScheduleAJAXEditForm(AJAXEditForm, LoopTaskScheduleEditForm): + """Loop-style task schedule edit form, AJAX view""" + + +class ITaskRunnerButtons(Interface): + """Task runner buttons""" + + close = CloseButton(name='close', title=_("Close")) + debug = button.Button(name='debug', title=_("Run in debug mode")) + execute = button.Button(name='execute', title=_("Run in normal mode")) + + +@pagelet_config(name='run.html', context=ITask, layer=IPyAMSLayer, permission='system.manage') +class TaskRunForm(AdminDialogEditForm): + """Task runner form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + dialog_class = 'modal-large' + legend = _("Execute task") + fields = field.Fields(Interface) + buttons = button.Buttons(ITaskRunnerButtons) + + ajax_handler = 'run.json' + + def updateActions(self): + super(TaskRunForm, self).updateActions() + if 'debug' in self.actions: + self.actions['debug'].addClass('btn-info') + if 'execute' in self.actions: + self.actions['execute'].addClass('btn-primary') + + def applyChanges(self, data): + task = self.getContent() + if self.actions['execute'].name in self.request.params: + task.launch() + else: + report = StringIO() + task.run(report) + return report.getvalue() + + +@view_config(name='run.json', context=ITask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class TaskRunAJAXForm(AJAXEditForm, TaskRunForm): + """Task runner form, AJAX view""" + + def get_ajax_output(self, changes): + translate = self.request.localizer.translate + if self.actions['execute'].name in self.request.params: + return {'status': 'success', + 'message': translate(_('Task scheduled in normal mode will start in 5 seconds...'))} + else: + return {'status': 'message', + 'content': {'raw': True, + 'text': changes, + 'target': '#task-debug-report'}, + 'message': translate(_('Task run in debug mode. Please check your console...')), + 'close_form': False} + + +@adapter_config(context=(ITask, IAdminLayer, TaskRunForm), provides=IContentHelp) +class TaskRunFormHelpAdapter(ContentHelp): + """Task run form help adapter""" + + status = 'warning' + header = _("Executing and debugging task") + message = _("""You can choose to execute the task in 'normal' mode or in 'debug' mode. + +In normal mode, the task is scheduled in a standard way and run in a background process (after 5 seconds). + +In debug mode, the task is run in the context of the main application process; the goal of this mode +is to allow a developer to insert breakpoints. + +**WARNING**: in both mode, the task will be executed even if it's disabled in it's scheduling settings!""") + message_format = 'rest' + + +@viewlet_config(name='task-debug-report', view=TaskRunForm, layer=IAdminLayer, + manager=IWidgetsPrefixViewletsManager) +@template_config(template='templates/task-debug-report.pt') +class TaskDebugReportViewlet(Viewlet): + """Task debug report viewlet""" + + +@pagelet_config(name='history.html', context=ITask, layer=IPyAMSLayer, permission='system.view') +class TaskHistoryDisplayForm(AdminDialogDisplayForm): + """Task history display form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + legend = _("Task history") + dialog_class = 'modal-max' + icon_css_class = 'fa fa-fw fa-history' + + fields = field.Fields(Interface) + + +STATUS_CLASS = {'OK': 'success', + 'Warning': 'warning', + 'Error': 'danger', + 'Empty': 'info'} + + +class TaskHistoryItemsTable(BaseTable): + """Task history items table""" + + title = _("Task history") + cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight datatable'} + sortOn = None + + @property + def data_attributes(self): + return {'table': {'data-ams-datatable-global-filter': 'false', + 'data-ams-datatable-info': 'false', + 'data-ams-datatable-sort': 'false', + 'data-ams-datatable-sdom': "t<'dt-row dt-bottom-row'<'text-right'p>>", + 'data-ams-datatable-display-length': '20', + 'data-ams-datatable-pagination-type': 'bootstrap_prevnext'}, + 'tr': {'data-ams-url': lambda x: absolute_url(x, self.request, 'info.json'), + 'data-ams-target': '#task-history-report'}} + + def getCSSHighlightClass(self, column, item, cssClass): + return STATUS_CLASS[item.status] + + +@adapter_config(name='name', context=(Interface, IAdminLayer, TaskHistoryItemsTable), provides=IColumn) +class TaskHistoryDateColumn(I18nColumn, GetAttrColumn): + """Task history date column""" + + _header = _("Date") + attrName = 'date' + + def renderCell(self, item): + return format_datetime(item.date, request=self.request) + + +@adapter_config(context=(ITask, IAdminLayer, TaskHistoryItemsTable), provides=IValues) +class TaskHistoryValuesAdapter(ContextRequestViewAdapter): + """Task history values adapter""" + + @property + def values(self): + return sorted(self.context.history.values(), + key=lambda x: x.date, + reverse=True) + + +@viewlet_config(name='task-history', view=TaskHistoryDisplayForm, layer=IAdminLayer, + manager=IWidgetsSuffixViewletsManager) +@template_config(template='templates/task-history.pt') +class TaskHistoryViewlet(Viewlet): + """Task history viewlet""" + + table = TaskHistoryItemsTable + + def __init__(self, context, request, view, manager): + super(TaskHistoryViewlet, self).__init__(context, request, view, manager) + self.table = TaskHistoryItemsTable(context, request) + + def update(self): + super(TaskHistoryViewlet, self).update() + self.table.update() + + +@view_config(name='info.json', context=ITaskHistory, request_type=IPyAMSLayer, + permission='system.view', renderer='json', xhr=True) +def TaskHistoryInfoView(request): + return {'status': 'success', + 'close_form': False, + 'content': {'raw': True, + 'text': request.context.report, + 'target': '#task-history-report'}} diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/templates/scheduler-jobs.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/templates/scheduler-jobs.pt Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,3 @@ +
+ +
diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/templates/task-debug-report.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/templates/task-debug-report.pt Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/templates/task-history.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/templates/task-history.pt Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,9 @@ +
+
+ +
+
+ +
+
diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/url.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/url.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,89 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces.url import IURLCallerTask, IURLCallerTaskInfo +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from zope.component.interfaces import ISite + +# import packages +from pyams_form.form import AJAXAddForm, AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_scheduler.url import URLCallerTask +from pyams_scheduler.zmi.scheduler import SchedulerTasksTable +from pyams_scheduler.zmi.task import TaskBaseAddForm +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogEditForm +from pyramid.view import view_config +from z3c.form import field + +from pyams_scheduler import _ + + +@viewlet_config(name='add-scheduler-url-task.menu', context=ISite, layer=IPyAMSLayer, + view=SchedulerTasksTable, manager=IToolbarAddingMenu, + permission='system.manage', weight=5) +class URLTaskAddMenu(ToolbarMenuItem): + """URL caller task add menu""" + + label = _("Add URL caller task...") + label_css_class = 'fa fa-fw fa-globe' + url = 'add-scheduler-url-task.html' + modal_target = True + + +@pagelet_config(name='add-scheduler-url-task.html', context=ISite, layer=IPyAMSLayer, + permission='system.manage') +class URLTaskAddForm(TaskBaseAddForm): + """URL caller task add form""" + + legend = _("Add URL caller task") + icon_css_class = 'fa fa-fw fa-globe' + + ajax_handler = 'add-scheduler-url-task.json' + task_factory = URLCallerTask + + +@view_config(name='add-scheduler-url-task.json', context=ISite, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class URLTaskAJAXAddForm(AJAXAddForm, URLTaskAddForm): + """URL caller task add form, AJAX view""" + + +@pagelet_config(name='settings.html', context=IURLCallerTask, layer=IPyAMSLayer, permission='system.view') +class URLTaskEditForm(AdminDialogEditForm): + """URL caller task edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + legend = _("Edit task settings") + icon_css_class = 'fa fa-fw fa-globe' + + fields = field.Fields(IURLCallerTaskInfo) + ajax_handler = 'settings.json' + edit_permission = 'system.manage' + + +@view_config(name='settings.json', context=IURLCallerTask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class URLTaskAJAXEditForm(AJAXEditForm, URLTaskEditForm): + """URL caller task edit form, AJAX view""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zmi/zodb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zmi/zodb.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,89 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces.zodb import IZODBPackingTask, IZODBPackingTaskInfo +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from zope.component.interfaces import ISite + +# import packages +from pyams_form.form import AJAXAddForm, AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_scheduler.zmi.scheduler import SchedulerTasksTable +from pyams_scheduler.zmi.task import TaskBaseAddForm +from pyams_scheduler.zodb import ZODBPackingTask +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogEditForm +from pyramid.view import view_config +from z3c.form import field + +from pyams_scheduler import _ + + +@viewlet_config(name='add-scheduler-zodb-task.menu', context=ISite, layer=IPyAMSLayer, + view=SchedulerTasksTable, manager=IToolbarAddingMenu, + permission='system.manage', weight=5) +class ZODBTaskAddMenu(ToolbarMenuItem): + """ZODB packing task add menu""" + + label = _("Add ZODB packing task...") + label_css_class = 'fa fa-fw fa-database' + url = 'add-scheduler-zodb-task.html' + modal_target = True + + +@pagelet_config(name='add-scheduler-zodb-task.html', context=ISite, layer=IPyAMSLayer, + permission='system.manage') +class ZODBTaskAddForm(TaskBaseAddForm): + """ZODB packing task add form""" + + legend = _("Add ZODB packing task") + icon_css_class = 'fa fa-fw fa-database' + + ajax_handler = 'add-scheduler-zodb-task.json' + task_factory = ZODBPackingTask + + +@view_config(name='add-scheduler-zodb-task.json', context=ISite, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class ZODBTaskAJAXAddForm(AJAXAddForm, ZODBTaskAddForm): + """ZODB packing task add form, AJAX view""" + + +@pagelet_config(name='settings.html', context=IZODBPackingTask, layer=IPyAMSLayer, permission='system.view') +class ZODBTaskEditForm(AdminDialogEditForm): + """ZODB packing task edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("Scheduler task: {0}")).format(self.context.name) + + legend = _("Edit task settings") + icon_css_class = 'fa fa-fw fa-database' + + fields = field.Fields(IZODBPackingTaskInfo) + ajax_handler = 'settings.json' + edit_permission = 'system.manage' + + +@view_config(name='settings.json', context=IZODBPackingTask, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class ZODBTaskAJAXEditForm(AJAXEditForm, ZODBTaskEditForm): + """ZODB packing task edit form, AJAX view""" diff -r 000000000000 -r 48483b0b26fa src/pyams_scheduler/zodb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_scheduler/zodb.py Wed Mar 11 11:52:59 2015 +0100 @@ -0,0 +1,51 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_scheduler.interfaces.zodb import IZODBPackingTask +from pyams_utils.interfaces.zeo import IZEOConnection +from zope.component.interfaces import ISite + +# import packages +from pyams_scheduler.task import Task +from pyams_utils.traversing import get_parent +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IZODBPackingTask) +class ZODBPackingTask(Task): + """ZODB packing task""" + + zeo_connection = FieldProperty(IZODBPackingTask['zeo_connection']) + pack_time = FieldProperty(IZODBPackingTask['pack_time']) + + def run(self, report): + site = get_parent(self, ISite) + sm = site.getSiteManager() + zeo_connection = sm.queryUtility(IZEOConnection, self.zeo_connection) + if zeo_connection is None: + report.write("Can't find ZEO connection. Task aborted.") + return + report.write("Loaded ZEO connection {0}\n".format(self.zeo_connection)) + report.write("Packing transactions older than {0} days\n".format(self.pack_time)) + storage, db = zeo_connection.get_connection(get_storage=True) + try: + db.pack(days=self.pack_time) + report.write("\nPack successful.\n") + finally: + storage.close()