├── .gitignore ├── .readthedocs.yaml ├── CHANGES.txt ├── MANIFEST.in ├── README.rst ├── doc ├── .gitignore ├── Makefile ├── api_client.rst ├── api_internal.rst ├── api_notification.rst ├── api_server.rst ├── api_service.rst ├── api_store.rst ├── api_util.rst ├── api_web.rst ├── client.rst ├── conf.py ├── crab.ini ├── crabd.ini ├── index.rst ├── install.rst ├── schema.sql ├── schema_mysql.sed ├── server.rst └── web.rst ├── lib └── crab │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── notify │ ├── __init__.py │ └── email.py │ ├── report │ ├── __init__.py │ ├── html.py │ ├── summary.py │ └── text.py │ ├── server │ ├── __init__.py │ ├── config.py │ └── io.py │ ├── service │ ├── __init__.py │ ├── clean.py │ ├── monitor.py │ └── notify.py │ ├── store │ ├── __init__.py │ ├── db.py │ ├── file.py │ ├── mysql.py │ └── sqlite.py │ ├── util │ ├── __init__.py │ ├── bus.py │ ├── compat.py │ ├── crontab.py │ ├── datetime.py │ ├── filter.py │ ├── guesstimezone.py │ ├── pid.py │ ├── schedule.py │ ├── statuspattern.py │ ├── string.py │ └── web.py │ ├── version.py │ └── web │ ├── __init__.py │ └── web.py ├── requirements.txt ├── res ├── coloroutput.js ├── crab.css ├── crab.js ├── crontabs.js ├── editnotify.js ├── favicon-disconnect.png ├── favicon-error.png ├── favicon-stopped.png ├── favicon-warn.png ├── favicon.png ├── jobevents.js └── joblist.js ├── scripts ├── crab ├── crabd ├── crabd-check └── crabsh ├── setup.py ├── templ ├── base.html ├── confirm.html ├── crontabs.html ├── dynres │ └── crabutil.js ├── editnotify.html ├── job.html ├── jobconfig.html ├── jobevents.html ├── joblist.html ├── joboutput.html └── report │ └── basic.html ├── test ├── __init__.py ├── test_crontab.py ├── test_io.py ├── test_schedule.py ├── test_store.py ├── test_storethread.py ├── test_type.py └── test_util.py └── util ├── fromoutputstore.py ├── tooutputstore.py ├── update_2012-10-15.sql ├── update_2014-07-09.sql ├── update_2014-08-05.sql ├── update_2016-01-06_mysql.sql └── update_2016-01-06_sqlite.sql /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | /MANIFEST 4 | /build 5 | /dist 6 | /doc/schema_mysql.sql 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: doc/conf.py 13 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | Next release 2 | 3 | - Added a "quiet" option to prevent crabsh issuing fallback messages 4 | to standard output except in the case of failure. This can be enabled 5 | via the CRABQUIET variable or crabsh.quiet configuration parameter. 6 | - Now support Font Awesome version 6. Existing installations of 7 | the Crab server being updated will also need an updated Font Awesome. 8 | - Removed support for an RSS feed. 9 | 10 | 0.5.1, 2021-08-05 11 | 12 | - Now support Font Awesome version 5. Existing installations of 13 | the Crab server being updated will also need an updated Font 14 | Awesome, including renaming the res/fonts directory again to 15 | res/webfonts. 16 | - Added crabd options to specify where to write access and error 17 | logs. 18 | - Added crabd --daemon option which uses CherryPy's daemonizer 19 | plugin to run the server in the background. 20 | - Added crabd --passive option to run the server with a passive monitor 21 | and no other services. 22 | - Crab clients can now re-try connections to the server. 23 | - Wrapper shell crabsh now uses environment variable CRABWATCHDOG 24 | to set a timeout (in minutes) for the command being run. 25 | If this time is exceeded, it will try to kill the command, 26 | collect its output and then report a new "watchdog" status. 27 | (Requires Python 3.3 or the subprocess32 backport module.) 28 | 29 | 0.5.0, 2016-01-27 30 | 31 | - Job configuration expanded to include status patterns and a note. 32 | These patterns are regular expressions to be compared to the job output. 33 | (A SQLite update script is provided: util/update_2014-07-09.sql.) 34 | - Client timeout added for communication with server (Python 2.6+ only). 35 | - Ability to "inhibit" the execution of Cron jobs added. 36 | (A SQLite update script is provided: util/update_2014-08-05.sql.) 37 | - Support for MySQL added as an alternative to SQLite. 38 | - Added crabd options to import and export job configuration. 39 | - Added 30 second delay before marking jobs as "late". 40 | - Check if /etc/localtime is a symlink when trying to guess a client's 41 | timezone in case the system has multiple identical timezones. 42 | - A new clean service can remove old events from storage. 43 | (When job output is stored in the database, this requires a 44 | minor schema update. SQLite and MySQL update scripts are provided: 45 | util/update_2016-01-06_sqlite.sql and util/update_2016-01-06_mysql.sql.) 46 | - Wrapper shell crabsh now writes its own PID to the pidfile immediately 47 | (rather than that of the child process after a 5 second delay) 48 | and also now applies the pidfile check even when CRABIGNORE is set. 49 | 50 | 0.4.2, 2014-02-21 51 | 52 | - Now support Font Awesome version 4. Since this version breaks 53 | compatability with version 3, existing installations of the Crab 54 | server being updated will also need an updated Font Awesome, 55 | including renaming the res/font directory to res/fonts. 56 | - Added internal pidfile support to crabd. This uses the 57 | crab.util.pid module which is also used by crabsh. 58 | 59 | 0.4.1, 2013-09-04 60 | 61 | - Avoid an error when two threads attempt to create the same 62 | directory at the same time. 63 | 64 | 0.4.0, 2013-08-29 65 | 66 | - When used to report job finish events, the crab utility now 67 | supports --stdout and --stderr options. 68 | - A new status code 'WARNING' has been added for generic 69 | client-generated warnings for which 'UNKNOWN' is not appropriate. 70 | - Jobs can be marked as deleted and job identifiers can be changed 71 | via the web interface. 72 | - A new environment variable CRABUSERCONFIG can be set to change 73 | the directory searched for user level configuration files. 74 | - Job output with ANSI colors can be colored if the ansi_up 75 | JavaScipt library is installed. 76 | - Times related to jobs without specified timezones are now shown 77 | in a common timezone instead of as raw database output. 78 | - Signals SIGPIPE and SIGCONT/XFSZ are restored by the crabsh wrapper 79 | script before invoking commands in versions of Python before 3.2. 80 | - An edit command has been added to the crab utility as a convenient 81 | alternative to running "crontab -e" and then "crab import". 82 | 83 | 0.3.0, 2013-03-28 84 | 85 | - Empty job output is no longer stored. 86 | - JavaScript uses RegExp instead of replace with 'g' option to avoid 87 | problems with browsers which do not support it. 88 | - Two new status codes have been added: 'CLEARED' and 'ALREADYRUNNING'. 89 | - The web interface now provides an option to clear the status of 90 | a job, returning it to a green color on the dashboard by inserting a 91 | 'CLEARED' event. 92 | - Added a basic pidfile module which crabsh can use to detect that a job 93 | is already running and send an 'ALREADYRUNNING' status instead of 94 | starting it again. 95 | - Improved parsing of CRAB variables with the intention that crabsh 96 | and crabd both consisently combine variables from the environment 97 | with those specified at the start of a job command line. 98 | - CRABIGNORE now prevents crabsh reporting the status of a job. 99 | - PyRSS2Gen is now an optional dependency, and if it is not present, 100 | the RSS button will not be shown. 101 | 102 | 0.2.0, 2012-10-17 103 | 104 | - Minor update to the database schema to replace confusing names. 105 | (A corresponding SQLite update script is provided in the util directory.) 106 | - Job output and raw crontabs can now optionally be saved in files instead 107 | of in the database. 108 | - The base URL used in email notifications and the RSS feed is now 109 | configurable. 110 | - Added history navigation to the job pages. 111 | - Headings on the dashboard page can be clicked to sort the table. 112 | 113 | 0.1.0, 2012-10-05 114 | 115 | - Initial release. 116 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include MANIFEST.in 3 | include test/*.py 4 | include util/fromoutputstore.py 5 | include util/tooutputstore.py 6 | include util/update_2012-10-15.sql 7 | include util/update_2014-07-09.sql 8 | include util/update_2014-08-05.sql 9 | include util/update_2016-01-06_mysql.sql 10 | include util/update_2016-01-06_sqlite.sql 11 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | schema_mysql.sql: schema_mysql.sed schema.sql 2 | sed -f $^ > $@ 3 | -------------------------------------------------------------------------------- /doc/api_client.rst: -------------------------------------------------------------------------------- 1 | Crab Client API 2 | =============== 3 | 4 | crab.client module 5 | ------------------ 6 | 7 | .. automodule:: crab.client 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | :special-members: 12 | 13 | crab module 14 | ----------- 15 | 16 | .. automodule:: crab 17 | :members: 18 | :member-order: bysource 19 | :undoc-members: 20 | -------------------------------------------------------------------------------- /doc/api_internal.rst: -------------------------------------------------------------------------------- 1 | Internal API 2 | ============ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | api_server 8 | api_notification 9 | api_service 10 | api_store 11 | api_util 12 | api_web 13 | -------------------------------------------------------------------------------- /doc/api_notification.rst: -------------------------------------------------------------------------------- 1 | Notifications 2 | ============= 3 | 4 | crab.notify 5 | ----------- 6 | 7 | .. automodule:: crab.notify 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | 12 | crab.notify.email 13 | ----------------- 14 | 15 | .. automodule:: crab.notify.email 16 | :members: 17 | :member-order: bysource 18 | :undoc-members: 19 | 20 | crab.report 21 | ----------- 22 | 23 | .. automodule:: crab.report 24 | :members: 25 | :member-order: bysource 26 | :undoc-members: 27 | 28 | crab.report.text 29 | ---------------- 30 | 31 | .. automodule:: crab.report.text 32 | :members: 33 | :member-order: bysource 34 | :undoc-members: 35 | 36 | crab.report.html 37 | ---------------- 38 | 39 | .. automodule:: crab.report.html 40 | :members: 41 | :member-order: bysource 42 | :undoc-members: 43 | -------------------------------------------------------------------------------- /doc/api_server.rst: -------------------------------------------------------------------------------- 1 | Crab Server 2 | =========== 3 | 4 | crab.server 5 | ----------- 6 | 7 | .. automodule:: crab.server 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | 12 | crab.server.config 13 | ------------------ 14 | 15 | .. automodule:: crab.server.config 16 | :members: 17 | :member-order: bysource 18 | :undoc-members: 19 | 20 | crab.server.io 21 | -------------- 22 | 23 | .. automodule:: crab.server.io 24 | :members: 25 | :member-order: bysource 26 | :undoc-members: 27 | -------------------------------------------------------------------------------- /doc/api_service.rst: -------------------------------------------------------------------------------- 1 | Services 2 | ======== 3 | 4 | crab.service 5 | ------------ 6 | 7 | .. automodule:: crab.service 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | 12 | crab.service.clean 13 | ------------------ 14 | 15 | .. automodule:: crab.service.clean 16 | :members: 17 | :member-order: bysource 18 | :undoc-members: 19 | 20 | crab.service.monitor 21 | -------------------- 22 | 23 | .. automodule:: crab.service.monitor 24 | :members: 25 | :member-order: bysource 26 | :undoc-members: 27 | 28 | crab.service.notify 29 | ------------------- 30 | 31 | .. automodule:: crab.service.notify 32 | :members: 33 | :member-order: bysource 34 | :undoc-members: 35 | -------------------------------------------------------------------------------- /doc/api_store.rst: -------------------------------------------------------------------------------- 1 | Storage 2 | ======= 3 | 4 | crab.store 5 | ---------- 6 | 7 | .. automodule:: crab.store 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | 12 | crab.store.db 13 | ------------- 14 | 15 | .. automodule:: crab.store.db 16 | :members: 17 | :member-order: bysource 18 | :undoc-members: 19 | 20 | crab.store.file 21 | --------------- 22 | 23 | .. automodule:: crab.store.file 24 | :members: 25 | :member-order: bysource 26 | :undoc-members: 27 | 28 | crab.store.mysql 29 | ---------------- 30 | 31 | .. automodule:: crab.store.mysql 32 | :members: 33 | :member-order: bysource 34 | :undoc-members: 35 | 36 | crab.store.sqlite 37 | ----------------- 38 | 39 | .. automodule:: crab.store.sqlite 40 | :members: 41 | :member-order: bysource 42 | :undoc-members: 43 | -------------------------------------------------------------------------------- /doc/api_util.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | crab.util.bus 5 | ------------- 6 | 7 | .. automodule:: crab.util.bus 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | 12 | crab.util.compat 13 | ---------------- 14 | 15 | .. automodule:: crab.util.compat 16 | :members: 17 | :member-order: bysource 18 | :undoc-members: 19 | 20 | crab.util.filter 21 | ---------------- 22 | 23 | .. automodule:: crab.util.filter 24 | :members: 25 | :member-order: bysource 26 | :undoc-members: 27 | 28 | crab.util.guesstimezone 29 | ----------------------- 30 | 31 | .. automodule:: crab.util.guesstimezone 32 | :members: 33 | :member-order: bysource 34 | :undoc-members: 35 | 36 | crab.util.pid 37 | ------------- 38 | 39 | .. automodule:: crab.util.pid 40 | :members: 41 | :member-order: bysource 42 | :undoc-members: 43 | 44 | crab.util.schedule 45 | ------------------ 46 | 47 | .. automodule:: crab.util.schedule 48 | :members: 49 | :member-order: bysource 50 | :undoc-members: 51 | 52 | crab.util.statuspattern 53 | ----------------------- 54 | 55 | .. automodule:: crab.util.statuspattern 56 | :members: 57 | :member-order: bysource 58 | :undoc-members: 59 | 60 | crab.util.string 61 | ---------------- 62 | 63 | .. automodule:: crab.util.string 64 | :members: 65 | :member-order: bysource 66 | :undoc-members: 67 | 68 | crab.util.web 69 | ------------- 70 | 71 | .. automodule:: crab.util.web 72 | :members: 73 | :member-order: bysource 74 | :undoc-members: 75 | -------------------------------------------------------------------------------- /doc/api_web.rst: -------------------------------------------------------------------------------- 1 | Web 2 | === 3 | 4 | crab.web.web 5 | ------------ 6 | 7 | .. automodule:: crab.web.web 8 | :members: 9 | :member-order: bysource 10 | :undoc-members: 11 | -------------------------------------------------------------------------------- /doc/client.rst: -------------------------------------------------------------------------------- 1 | Client Configuration 2 | ==================== 3 | 4 | .. include:: ../README.rst 5 | :start-after: .. startcrabclient 6 | :end-before: .. endcrabclient 7 | 8 | Example Configuration File 9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | Here is the example client configuration file ``crab.ini`` which is 12 | distributed with Crab: 13 | 14 | .. literalinclude:: crab.ini 15 | :language: ini 16 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Crab documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Feb 18 11:57:21 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../lib')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Crab' 44 | copyright = \ 45 | u'2015-2016, East Asian Observatory, ' \ 46 | u'2012-2014, Science and Technology Facilities Council' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | from crab.version import version 54 | # The full version, including alpha/beta/rc tags. 55 | release = version 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'default' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'crabdoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | latex_elements = { 175 | # The paper size ('letterpaper' or 'a4paper'). 176 | #'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'crab.tex', u'Crab Documentation', 189 | u'Graham Bell', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'crab', u'Crab Documentation', 219 | [u'Graham Bell'], 1) 220 | ] 221 | 222 | # If true, show URL addresses after external links. 223 | #man_show_urls = False 224 | 225 | 226 | # -- Options for Texinfo output ------------------------------------------------ 227 | 228 | # Grouping the document tree into Texinfo files. List of tuples 229 | # (source start file, target name, title, author, 230 | # dir menu entry, description, category) 231 | texinfo_documents = [ 232 | ('index', 'Crab', u'Crab Documentation', 233 | u'Graham Bell', 'Crab', 'Cron Dashboard.', 234 | 'Miscellaneous'), 235 | ] 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #texinfo_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #texinfo_domain_indices = True 242 | 243 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 244 | #texinfo_show_urls = 'footnote' 245 | -------------------------------------------------------------------------------- /doc/crab.ini: -------------------------------------------------------------------------------- 1 | # Configure how to connect to the server where the Crab daemon is running. 2 | [server] 3 | host = localhost 4 | port = 8000 5 | # Timeout for connection to the server (seconds). 6 | # timeout = 30 7 | # Number of times to attempt to connect to server. 8 | # max_tries = 1 9 | # Delay between connection attempts (seconds). 10 | # retry_delay = 5 11 | # Timeout for communication with the server (seconds). 12 | # If not specified, the "timeout" values applies to communication also. 13 | # comm_timeout = 30 14 | 15 | # Configuration for this client. 16 | [client] 17 | # Override hostname provided by OS if required. 18 | # hostname = host 19 | 20 | # Override username provided by OS if required. 21 | # username = user 22 | 23 | # Attempt to use fully qualified domain name for client (if not specified). 24 | # use_fqdn = false 25 | 26 | # Configure crabsh behavior. 27 | [crabsh] 28 | # Choose whether to honor the inhibit message on job start. 29 | # allow_inhibit = true 30 | # Write fallback messages to standard output only on failure. 31 | # quiet = false 32 | -------------------------------------------------------------------------------- /doc/crabd.ini: -------------------------------------------------------------------------------- 1 | # This file is read by CherryPy rather than ConfigParser 2 | # and the following differences apply: strings must be 3 | # quoted, and it appears that if you include a section, 4 | # you must include all settings in that section as the 5 | # defaults are not kept. 6 | 7 | # [crab] 8 | # # Directory in which to find the res/ and templ/ directories. 9 | # home = '/usr/share/crab' 10 | # 11 | # # Base URL to use when generating links to be used from 12 | # # outside the Crab web interface, e.g. in notification 13 | # # emails. 14 | # base_url = 'http://crabserver.example.com:8000' 15 | # # To generate automatically: 16 | # base_url = None 17 | 18 | # [store] 19 | # # Main storage backend. 20 | # type = 'sqlite' 21 | # file = '/var/lib/crab/crab.db' 22 | # # Alternatively for MySQL: 23 | # # type = 'mysql' 24 | # # host = 'localhost' 25 | # # database = 'crab' 26 | # # user = 'crab' 27 | # # password = 'crab' 28 | 29 | # [outputstore] 30 | # # Storage backend to be used for storing job output 31 | # # and raw crontabs. 32 | # # (This is optional, unless the selected main backend 33 | # # is not capable of storing output.) 34 | # type = 'file' 35 | # dir = '/var/lib/crab' 36 | 37 | # [global] 38 | # engine.autoreload.on = False 39 | # 40 | # server.socket_port = 8000 41 | # 42 | # # To listen on localhost only: 43 | # server.socket_host = '127.0.0.1' 44 | # 45 | # # To listen on a specific address: 46 | # server.socket_host = '0.0.0.0' 47 | 48 | # [email] 49 | # # Server through which to send email notifications. 50 | # server = 'mailhost' 51 | # 52 | # # Name (and address) to send email from. 53 | # from = 'Crab Daemon' 54 | # 55 | # # Subjects to use for different severity levels. 56 | # subject_ok = 'Crab notification' 57 | # subject_warning = 'Crab notification (WARNING)' 58 | # subject_error = 'Crab notification (ERROR)' 59 | 60 | # [notify] 61 | # # Cron-style schedule for sending "daily" notifications, 62 | # # to be used for notifications without specified schedules. 63 | # daily = '0 0 * * *' 64 | # 65 | # # Timezone to use for the daily notification schedule. 66 | # timezone = 'UTC' 67 | 68 | # # Uncomment this section if you wish to use the automated cleaning 69 | # # service to delete the history of old events. 70 | # [clean] 71 | # # Cron-style schedule for cleaning operations. 72 | # schedule = '15 0 * * *' 73 | # # Timezone to use for the cleaning schedule. 74 | # timezone = 'UTC' 75 | # # Number of days for which to keep events. 76 | # keep_days = 90 77 | 78 | # # This section applies if crabd is run with the --accesslog option 79 | # # giving the base access log file name (e.g. via crabd-check). 80 | # [access_log] 81 | # # Maximum size of log files (MiB), or 0 to disable rotation. 82 | # max_size = 10 83 | # # Number of past log files to keep, or 0 to disable rotation. 84 | # backup_count = 10 85 | 86 | # # This section applies if crabd is run with the --errorlog option 87 | # # giving the base error log file name (e.g. via crabd-check). 88 | # [error_log] 89 | # # Maximum size of log files (MiB), or 0 to disable rotation. 90 | # max_size = 10 91 | # # Number of past log files to keep, or 0 to disable rotation. 92 | # backup_count = 10 93 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Crab 2 | ==== 3 | 4 | .. include:: ../README.rst 5 | :start-after: .. startcrabintro 6 | :end-before: .. endcrabintro 7 | 8 | Contents 9 | -------- 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | install 15 | server 16 | client 17 | web 18 | api_client 19 | api_internal 20 | 21 | Indices and tables 22 | ------------------ 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. include:: ../README.rst 5 | :start-after: .. startcrabinstall 6 | :end-before: .. endcrabinstall 7 | -------------------------------------------------------------------------------- /doc/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE job ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | host VARCHAR(255) NOT NULL, 4 | user VARCHAR(255) NOT NULL, 5 | command VARCHAR(255) NOT NULL, 6 | crabid VARCHAR(255), 7 | time VARCHAR(255), 8 | timezone VARCHAR(255), 9 | installed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | deleted TIMESTAMP NULL, 11 | 12 | UNIQUE (host, user, crabid) 13 | ) 14 | -- MySQL: ENGINE=InnoDB 15 | ; 16 | 17 | CREATE INDEX job_crabid ON job (crabid); 18 | CREATE INDEX job_host ON job (host); 19 | CREATE INDEX job_user ON job (user); 20 | CREATE INDEX job_command ON job (command); 21 | CREATE INDEX job_installed ON job (installed); 22 | CREATE INDEX job_deleted ON job (deleted); 23 | 24 | CREATE TABLE jobstart ( 25 | id INTEGER PRIMARY KEY AUTOINCREMENT, 26 | jobid INTEGER NOT NULL, 27 | command VARCHAR(255) NOT NULL, 28 | datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | 30 | FOREIGN KEY (jobid) REFERENCES job(id) 31 | ON DELETE RESTRICT ON UPDATE RESTRICT 32 | ) 33 | -- MySQL: ENGINE=InnoDB 34 | ; 35 | 36 | CREATE INDEX jobstart_jobid ON jobstart (jobid); 37 | CREATE INDEX jobstart_datetime ON jobstart (datetime); 38 | 39 | CREATE TABLE jobfinish ( 40 | id INTEGER PRIMARY KEY AUTOINCREMENT, 41 | jobid INTEGER NOT NULL, 42 | command VARCHAR(255) NOT NULL, 43 | datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 44 | status INTEGER NOT NULL, 45 | 46 | FOREIGN KEY (jobid) REFERENCES job(id) 47 | ON DELETE RESTRICT ON UPDATE RESTRICT 48 | ) 49 | -- MySQL: ENGINE=InnoDB 50 | ; 51 | 52 | CREATE INDEX jobfinish_jobid ON jobfinish (jobid); 53 | CREATE INDEX jobfinish_datetime ON jobfinish (datetime); 54 | 55 | CREATE TABLE jobalarm ( 56 | id INTEGER PRIMARY KEY AUTOINCREMENT, 57 | jobid INTEGER NOT NULL, 58 | datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 59 | status INTEGER NOT NULL, 60 | 61 | FOREIGN KEY (jobid) REFERENCES job(id) 62 | ON DELETE RESTRICT ON UPDATE RESTRICT 63 | ) 64 | -- MySQL: ENGINE=InnoDB 65 | ; 66 | 67 | CREATE INDEX jobalarm_jobid ON jobalarm (jobid); 68 | CREATE INDEX jobalarm_datetime ON jobalarm (datetime); 69 | 70 | CREATE TABLE joboutput ( 71 | id INTEGER PRIMARY KEY AUTOINCREMENT, 72 | finishid INTEGER NOT NULL, 73 | stdout TEXT DEFAULT "" NOT NULL, 74 | stderr TEXT DEFAULT "" NOT NULL, 75 | 76 | UNIQUE (finishid), 77 | FOREIGN KEY (finishid) REFERENCES jobfinish(id) 78 | ON DELETE CASCADE ON UPDATE RESTRICT 79 | ) 80 | -- MySQL: ENGINE=InnoDB 81 | ; 82 | 83 | CREATE TABLE jobconfig ( 84 | id INTEGER PRIMARY KEY AUTOINCREMENT, 85 | jobid INTEGER NOT NULL, 86 | graceperiod INTEGER, 87 | timeout INTEGER, 88 | success_pattern VARCHAR(255) DEFAULT NULL, 89 | warning_pattern VARCHAR(255) DEFAULT NULL, 90 | fail_pattern VARCHAR(255) DEFAULT NULL, 91 | note TEXT DEFAULT NULL, 92 | inhibit BOOLEAN NOT NULL DEFAULT 0, 93 | 94 | UNIQUE (jobid), 95 | FOREIGN KEY (jobid) REFERENCES job(id) 96 | ON DELETE RESTRICT ON UPDATE RESTRICT 97 | ) 98 | -- MySQL: ENGINE=InnoDB 99 | ; 100 | 101 | CREATE TABLE jobnotify ( 102 | id INTEGER PRIMARY KEY AUTOINCREMENT, 103 | configid INTEGER, 104 | host VARCHAR(255) DEFAULT NULL, 105 | user VARCHAR(255) DEFAULT NULL, 106 | method VARCHAR(255) NOT NULL, 107 | address VARCHAR(255) NOT NULL, 108 | time VARCHAR(255) DEFAULT NULL, 109 | timezone VARCHAR(255) DEFAULT NULL, 110 | skip_ok BOOLEAN NOT NULL DEFAULT 0, 111 | skip_warning BOOLEAN NOT NULL DEFAULT 0, 112 | skip_error BOOLEAN NOT NULL DEFAULT 0, 113 | include_output BOOLEAN NOT NULL DEFAULT 0, 114 | 115 | FOREIGN KEY (configid) REFERENCES jobconfig(id) 116 | ON DELETE RESTRICT ON UPDATE RESTRICT 117 | ) 118 | -- MySQL: ENGINE=InnoDB 119 | ; 120 | 121 | CREATE INDEX jobnotify_host ON jobnotify (host); 122 | CREATE INDEX jobnotify_user ON jobnotify (user); 123 | 124 | CREATE TABLE rawcrontab ( 125 | id INTEGER PRIMARY KEY AUTOINCREMENT, 126 | host VARCHAR(255) NOT NULL, 127 | user VARCHAR(255) NOT NULL, 128 | crontab TEXT NOT NULL, 129 | 130 | UNIQUE (host, user) 131 | ) 132 | -- MySQL: ENGINE=InnoDB 133 | ; 134 | 135 | CREATE INDEX rawcrontab_host ON rawcrontab (host); 136 | CREATE INDEX rawcrontab_user ON rawcrontab (user); 137 | -------------------------------------------------------------------------------- /doc/schema_mysql.sed: -------------------------------------------------------------------------------- 1 | s/^-- MySQL: // 2 | s/AUTOINCREMENT/AUTO_INCREMENT/ 3 | -------------------------------------------------------------------------------- /doc/server.rst: -------------------------------------------------------------------------------- 1 | Server Configuration 2 | ==================== 3 | 4 | .. include:: ../README.rst 5 | :start-after: .. startcrabserver 6 | :end-before: .. endcrabserver 7 | 8 | Example Configuration File 9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | Here is the example server configuration file ``crabd.ini`` which is 12 | distributed with Crab: 13 | 14 | .. literalinclude:: crabd.ini 15 | :language: ini 16 | -------------------------------------------------------------------------------- /doc/web.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: .. startcrabweb 3 | :end-before: .. endcrabweb 4 | -------------------------------------------------------------------------------- /lib/crab/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2021 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | class CrabError(Exception): 19 | """Base class for exceptions raised internally by crab. 20 | 21 | Library functions should re-raise expected exceptions as a 22 | CrabError to allow them to be trapped conveniently without 23 | accidentally trapping other errors.""" 24 | pass 25 | 26 | 27 | class CrabStatus: 28 | """Helper class for status codes. 29 | 30 | The crab libraries should refer to codes by the symbolic 31 | names given in this class. 32 | 33 | VALUES is the set of status codes which should be accepted from 34 | a client. Other codes are for internal use, such as those 35 | generated by the monitor.""" 36 | 37 | SUCCESS = 0 38 | FAIL = 1 39 | UNKNOWN = 2 40 | COULDNOTSTART = 3 41 | ALREADYRUNNING = 4 42 | WARNING = 5 43 | INHIBITED = 6 44 | WATCHDOG = 7 45 | 46 | VALUES = set([SUCCESS, FAIL, UNKNOWN, COULDNOTSTART, ALREADYRUNNING, 47 | WARNING, INHIBITED, WATCHDOG]) 48 | 49 | # Additional internal status values (it is not valid for 50 | # a client to send these). Also some of these are less bad 51 | # than the client statuses. For example, if something has a 52 | # status of FAIL, you don't want to change it to just LATE. 53 | LATE = -1 54 | MISSED = -2 55 | TIMEOUT = -3 56 | CLEARED = -4 57 | 58 | INTERNAL_VALUES = set([LATE, MISSED, TIMEOUT, CLEARED]) 59 | 60 | _error_names = ['Succeeded', 'Failed', 'Unknown', 'Could not start', 61 | 'Already running', 'Warning', 'Inhibited', 'Watchdog'] 62 | _alarm_names = ['Late', 'Missed', 'Timed out', 'Cleared'] 63 | 64 | @staticmethod 65 | def get_name(status): 66 | """Returns a readable name for the given status code.""" 67 | try: 68 | if status is None: 69 | return 'Undefined' 70 | elif status >= 0: 71 | # TODO: find out if this can be referred to without class name? 72 | return CrabStatus._error_names[status] 73 | else: 74 | return CrabStatus._alarm_names[(-1) - status] 75 | except IndexError: 76 | return 'Status ' + str(status) 77 | 78 | @staticmethod 79 | def is_trivial(status): 80 | """Determines whether a status code is trivial and should 81 | mostly be ignored.""" 82 | return status in (CrabStatus.LATE, 83 | CrabStatus.CLEARED, 84 | CrabStatus.ALREADYRUNNING) 85 | 86 | @staticmethod 87 | def is_ok(status): 88 | """Returns true if the code does not indicate any kind of problem.""" 89 | return status == CrabStatus.SUCCESS or CrabStatus.is_trivial(status) 90 | 91 | @staticmethod 92 | def is_warning(status): 93 | """True if the given status is some kind of warning.""" 94 | return (status == CrabStatus.UNKNOWN or 95 | status == CrabStatus.MISSED or 96 | status == CrabStatus.WARNING or 97 | status == CrabStatus.INHIBITED) 98 | 99 | @staticmethod 100 | def is_error(status): 101 | """True if the given status is an error, i.e. not OK and not a 102 | warning.""" 103 | return not (CrabStatus.is_ok(status) or CrabStatus.is_warning(status)) 104 | 105 | 106 | class CrabEvent: 107 | """Helper class for crab events. 108 | 109 | Currently just provides symbolic names for the event types.""" 110 | START = 1 111 | ALARM = 2 112 | FINISH = 3 113 | 114 | _event_names = ['Started', 'Alarm', 'Finished'] 115 | 116 | @staticmethod 117 | def get_name(event): 118 | """Returns a readable name for the given event type code.""" 119 | 120 | try: 121 | return CrabEvent._event_names[event - 1] 122 | 123 | except IndexError: 124 | return 'Event ' + str(event) 125 | -------------------------------------------------------------------------------- /lib/crab/notify/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from collections import namedtuple 17 | from logging import getLogger 18 | 19 | from crab.report import CrabReportGenerator, CrabReportJob 20 | from crab.notify.email import CrabNotifyEmail 21 | 22 | CrabNotifyJob = namedtuple('CrabNotifyJob', ['n', 'start', 'end']) 23 | 24 | logger = getLogger(__name__) 25 | 26 | 27 | class CrabNotify: 28 | """Class for sending notification messages.""" 29 | 30 | def __init__(self, config, store): 31 | self.store = store 32 | 33 | self.send_email = CrabNotifyEmail(config['crab']['home'], 34 | config['crab']['base_url'], 35 | config['email']) 36 | 37 | def __call__(self, notifications): 38 | "Sends notification messages.""" 39 | 40 | report = CrabReportGenerator(self.store) 41 | 42 | for (jobs, keys) in self._group_notifications(notifications): 43 | output = report(jobs) 44 | if output is not None: 45 | email = [] 46 | 47 | for key in keys: 48 | (method, address) = key[0:2] 49 | 50 | if method == 'email': 51 | # In the case of email, we can build a list of 52 | # addresses and CC a single message to all of them. 53 | email.append(address) 54 | else: 55 | logger.error( 56 | 'Unknown notification method: {}'.format(method)) 57 | 58 | if email: 59 | self.send_email(output, email) 60 | 61 | def _group_notifications(self, notifications): 62 | """Constructs a list of notifications to be sent. 63 | 64 | Each item in the list consists of a tuple containing a tuple 65 | of CrabReportJob tuples and a set of (method, address) pairs. 66 | This allows each distinct report to be generated once, 67 | and then sent to a number of recipients.""" 68 | 69 | # First build a list of jobs to report on for each destination: 70 | notification = {} 71 | 72 | for entry in notifications: 73 | key = (entry.n['method'], entry.n['address'], 74 | entry.n['skip_ok'], entry.n['skip_warning'], 75 | entry.n['skip_error'], entry.n['include_output']) 76 | 77 | id_ = entry.n['id'] 78 | report_job = CrabReportJob(id_, entry.start, entry.end, 79 | entry.n['skip_ok'], 80 | entry.n['skip_warning'], 81 | entry.n['skip_error'], 82 | entry.n['include_output']) 83 | 84 | if key in notification: 85 | if id_ not in notification[key]: 86 | notification[key][id_] = report_job 87 | else: 88 | notification[key][id_] = report_job._replace( 89 | start=min(notification[key][id_].start, entry.start), 90 | end=max(notification[key][id_].end, entry.end)) 91 | 92 | else: 93 | notification[key] = {id_: report_job} 94 | 95 | # Then attempt to merge entries with the same job list: 96 | merged = {} 97 | 98 | for (key, jobs) in notification.items(): 99 | jobs = tuple(jobs.values()) 100 | 101 | if jobs not in merged: 102 | merged[jobs] = set([key]) 103 | else: 104 | merged[jobs].add(key) 105 | 106 | return merged.items() 107 | -------------------------------------------------------------------------------- /lib/crab/notify/email.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2016 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from __future__ import absolute_import 18 | 19 | from email.mime.multipart import MIMEMultipart 20 | from email.mime.text import MIMEText 21 | from email.utils import formatdate 22 | from smtplib import SMTP 23 | 24 | from crab.report.text import report_to_text 25 | from crab.report.html import report_to_html 26 | from crab.report.summary import report_to_summary 27 | 28 | 29 | class CrabNotifyEmail: 30 | """Class to send notification messages by email.""" 31 | 32 | def __init__(self, crab_home, base_url, config_email): 33 | """Construct a nofication object. 34 | 35 | Stores relevant configuration information in the object.""" 36 | 37 | self.home = crab_home 38 | self.base_url = base_url 39 | 40 | self.server = config_email['server'] 41 | self.from_ = config_email['from'] 42 | self.subject_ok = config_email['subject_ok'] 43 | self.subject_warning = config_email['subject_warning'] 44 | self.subject_error = config_email['subject_error'] 45 | 46 | def __call__(self, report, to): 47 | """Sends a report by email to the given addresses.""" 48 | 49 | if report.error: 50 | subject = self.subject_error 51 | elif report.warning: 52 | subject = self.subject_warning 53 | else: 54 | subject = self.subject_ok 55 | 56 | subject += ' (' + report_to_summary(report) + ')' 57 | 58 | message = MIMEMultipart('alternative') 59 | message['Subject'] = subject 60 | message['Date'] = formatdate(localtime=True) 61 | message['From'] = self.from_ 62 | message['To'] = ', '.join(to) 63 | 64 | message.attach(MIMEText(report_to_text(report), 'plain')) 65 | message.attach(MIMEText(report_to_html(report, 66 | self.home, self.base_url), 'html')) 67 | 68 | smtp = SMTP(self.server) 69 | smtp.sendmail(self.from_, to, message.as_string()) 70 | smtp.quit() 71 | -------------------------------------------------------------------------------- /lib/crab/report/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from collections import namedtuple 17 | 18 | from crab import CrabError, CrabStatus, CrabEvent 19 | from crab.util.filter import CrabEventFilter 20 | 21 | CrabReportJob = namedtuple( 22 | 'CrabReportJob', 23 | ['id_', 'start', 'end', 'skip_ok', 'skip_warning', 'skip_error', 24 | 'include_output']) 25 | 26 | CrabReport = namedtuple( 27 | 'CrabReport', 28 | ['num', 'error', 'warning', 'ok', 'info', 'events', 'stdout', 'stderr']) 29 | 30 | 31 | class CrabReportGenerator: 32 | """Class for generating reports on the operation of cron jobs. 33 | 34 | This class maintains a cache of job information and events 35 | to allow it to handle multiple report requests in an efficient 36 | manner. This depends on a single configuration, so methods 37 | for adjusting the filtering are not provided.""" 38 | 39 | def __init__(self, store, **kwargs): 40 | """Constructor for report object.""" 41 | 42 | self.store = store 43 | 44 | self.filter = CrabEventFilter(store, **kwargs) 45 | self.cache_info = {} 46 | self.cache_event = {} 47 | self.cache_error = {} 48 | self.cache_warning = {} 49 | self.cache_stdout = {} 50 | self.cache_stderr = {} 51 | 52 | def __call__(self, jobs): 53 | """Function call method, to process a list of jobs. 54 | 55 | Takes a list of jobs, which is a list of CrabReportJob 56 | tuples. 57 | 58 | Returns a CrabReport object including the number of jobs to be 59 | included in the report and sets of jobs in each state, 60 | or None if there are no entries to show.""" 61 | 62 | checked = set() 63 | error = set() 64 | warning = set() 65 | ok = set() 66 | num = 0 67 | report_info = {} 68 | report_events = {} 69 | report_stdout = {} 70 | report_stderr = {} 71 | 72 | for job in jobs: 73 | if job in checked: 74 | continue 75 | else: 76 | checked.add(job) 77 | 78 | (id_, start, end, skip_ok, skip_warning, skip_error, 79 | include_output) = job 80 | 81 | if id_ in self.cache_info: 82 | info = self.cache_info[id_] 83 | else: 84 | info = self.store.get_job_info(id_) 85 | if info is None: 86 | continue 87 | 88 | if info['crabid'] is None: 89 | info['title'] = info['command'] 90 | else: 91 | info['title'] = info['crabid'] 92 | 93 | self.cache_info[id_] = info 94 | 95 | if job in self.cache_event: 96 | events = self.cache_event[job] 97 | num_errors = self.cache_error[job] 98 | num_warnings = self.cache_warning[job] 99 | else: 100 | self.filter.set_timezone(info['timezone']) 101 | events = self.cache_event[job] = self.filter( 102 | self.store.get_job_events(id_, limit=None, 103 | start=start, end=end), 104 | skip_ok=skip_ok, skip_warning=skip_warning, 105 | skip_error=skip_error, skip_start=True) 106 | num_errors = self.cache_error[job] = self.filter.errors 107 | num_warnings = self.cache_warning[job] = self.filter.warnings 108 | 109 | if events: 110 | num += 1 111 | 112 | if num_errors: 113 | error.add(id_) 114 | elif num_warnings: 115 | warning.add(id_) 116 | else: 117 | ok.add(id_) 118 | 119 | report_info[id_] = info 120 | report_events[id_] = events 121 | 122 | if include_output: 123 | for event in events: 124 | if event['type'] == CrabEvent.FINISH: 125 | finishid = event['eventid'] 126 | if finishid in self.cache_stdout: 127 | report_stdout[finishid] = \ 128 | self.cache_stdout[finishid] 129 | report_stderr[finishid] = \ 130 | self.cache_stderr[finishid] 131 | else: 132 | (stdout, stderr) = self.store.get_job_output( 133 | finishid, info['host'], info['user'], 134 | id_, info['crabid']) 135 | 136 | report_stdout[finishid] = \ 137 | self.cache_stdout[finishid] = stdout 138 | report_stderr[finishid] = \ 139 | self.cache_stderr[finishid] = stderr 140 | 141 | if num: 142 | return CrabReport(num, error, warning, ok, 143 | report_info, report_events, 144 | report_stdout, report_stderr) 145 | else: 146 | return None 147 | -------------------------------------------------------------------------------- /lib/crab/report/html.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from mako.template import Template 17 | 18 | 19 | def report_to_html(report, home, base_url): 20 | template = Template(filename=home + '/templ/report/basic.html') 21 | return template.render(report=report, base_url=base_url) 22 | -------------------------------------------------------------------------------- /lib/crab/report/summary.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 East Asian Observatory. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | def report_to_summary(report, max_jobs=3, max_len=10): 18 | """ 19 | Generate a brief summary string for the given report. 20 | """ 21 | 22 | # Determine the jobs of most interest: if none, return "no jobs". 23 | if report.error: 24 | jobs = report.error 25 | 26 | elif report.warning: 27 | jobs = report.warning 28 | 29 | elif report.ok: 30 | jobs = report.ok 31 | 32 | else: 33 | return 'no jobs' 34 | 35 | # Are there too many jobs to list? 36 | if len(jobs) > max_jobs: 37 | return 'multiple jobs' 38 | 39 | # Truncate the job titles, allowing three characters for "..." when 40 | # indicating truncation. 41 | titles = [] 42 | crop_len = max(1, max_len - 3) 43 | 44 | for id_ in jobs: 45 | title = report.info[id_]['title'] 46 | 47 | if len(title) <= max_len: 48 | titles.append(title) 49 | 50 | else: 51 | titles.append(title[:crop_len] + '...') 52 | 53 | # Join the titles to create the summary. 54 | return ', '.join(sorted(titles)) 55 | -------------------------------------------------------------------------------- /lib/crab/report/text.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from crab import CrabEvent, CrabStatus 17 | 18 | 19 | def report_to_text(report, event_list=True): 20 | lines = [] 21 | sections = ['error', 'warning', 'ok'] 22 | titles = ['Jobs with Errors', 'Jobs with Warnings', 'Successful Jobs'] 23 | 24 | for (section, title) in zip(sections, titles): 25 | jobs = getattr(report, section) 26 | if jobs: 27 | lines.append(title) 28 | lines.append('=' * len(title)) 29 | lines.append('') 30 | for id_ in jobs: 31 | lines.append(' ' + _summary_line(report, id_)) 32 | lines.append('') 33 | 34 | if event_list: 35 | lines.append('Event Listing') 36 | lines.append('=============') 37 | lines.append('') 38 | 39 | for id_ in set.union(report.error, report.warning, report.ok): 40 | subhead = _summary_line(report, id_) 41 | lines.append(subhead) 42 | lines.append('-' * len(subhead)) 43 | lines.append('') 44 | 45 | for e in report.events[id_]: 46 | lines.append(' ' + _event_line(e)) 47 | 48 | if e['type'] == CrabEvent.FINISH: 49 | finishid = e['eventid'] 50 | if finishid in report.stdout and report.stdout[finishid]: 51 | lines.extend(_output_lines(8, 'Std. Out.', 52 | report.stdout[finishid])) 53 | if finishid in report.stderr and report.stderr[finishid]: 54 | lines.extend(_output_lines(8, 'Std. Error', 55 | report.stderr[finishid])) 56 | 57 | lines.append('') 58 | 59 | return "\n".join(lines) 60 | 61 | 62 | def _summary_line(report, id_): 63 | info = report.info[id_] 64 | return '{0:10} {1:10} {2}'.format(info['host'], info['user'], 65 | info['title']) 66 | 67 | 68 | def _event_line(event): 69 | return '{0:10} {1:10} {2}'.format(CrabEvent.get_name(event['type']), 70 | CrabStatus.get_name(event['status']), 71 | event['datetime']) 72 | 73 | 74 | def _output_lines(indent, title, text): 75 | lines = [] 76 | for line in text.strip().split('\n'): 77 | if lines: 78 | head = '' 79 | else: 80 | head = title 81 | lines.append('{0}{1:10} {2}'.format(' ' * indent, head, line)) 82 | 83 | return lines 84 | -------------------------------------------------------------------------------- /lib/crab/server/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2014 Science and Technology Facilities Council. 2 | # Copyright (C) 2018 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from codecs import latin_1_encode, latin_1_decode 18 | import json 19 | import socket 20 | 21 | import cherrypy 22 | from cherrypy import HTTPError 23 | 24 | from crab import CrabError, CrabStatus 25 | from crab.util.bus import CrabStoreListener 26 | 27 | 28 | class CrabServer(CrabStoreListener): 29 | """Crab server class, used for interaction with the client.""" 30 | 31 | def __init__(self, bus): 32 | """Constructor for CrabServer.""" 33 | 34 | super(CrabServer, self).__init__(bus) 35 | 36 | @cherrypy.expose 37 | def crontab(self, host, user, raw=False): 38 | """CherryPy handler for the crontab action. 39 | 40 | Allows the client to PUT a new crontab, or use a GET 41 | request to see a crontab-style representation of the 42 | job information held in the the storage backend.""" 43 | 44 | if cherrypy.request.method == 'GET': 45 | try: 46 | if raw: 47 | crontab = self.store.get_raw_crontab(host, user) 48 | else: 49 | crontab = self.store.get_crontab(host, user) 50 | return json.dumps({'crontab': crontab}) 51 | except CrabError as err: 52 | cherrypy.log.error('CrabError: read error: ' + str(err)) 53 | raise HTTPError(message='read error: ' + str(err)) 54 | 55 | elif cherrypy.request.method == 'PUT': 56 | try: 57 | data = self._read_json() 58 | crontab = data.get('crontab') 59 | 60 | if crontab is None: 61 | raise CrabError('no crontab received') 62 | 63 | warning = self.store.save_crontab( 64 | host, user, crontab, timezone=data.get('timezone')) 65 | 66 | return json.dumps({'warning': warning}) 67 | 68 | except CrabError as err: 69 | cherrypy.log.error('CrabError: write error: ' + str(err)) 70 | raise HTTPError(message='write error: ' + str(err)) 71 | 72 | @cherrypy.expose 73 | def start(self, host, user, crabid=None): 74 | """CherryPy handler allowing clients to report jobs starting.""" 75 | 76 | try: 77 | data = self._read_json() 78 | command = data.get('command') 79 | 80 | if command is None: 81 | raise CrabError('cron command not specified') 82 | 83 | data = self.store.log_start(host, user, crabid, command) 84 | 85 | return json.dumps({'inhibit': data['inhibit']}) 86 | 87 | except CrabError as err: 88 | cherrypy.log.error('CrabError: log error: ' + str(err)) 89 | raise HTTPError(message='log error: ' + str(err)) 90 | 91 | @cherrypy.expose 92 | def finish(self, host, user, crabid=None): 93 | """CherryPy handler allowing clients to report jobs finishing.""" 94 | 95 | try: 96 | data = self._read_json() 97 | command = data.get('command') 98 | status = data.get('status') 99 | 100 | if command is None or status is None: 101 | raise CrabError('insufficient information to log finish') 102 | 103 | if status not in CrabStatus.VALUES: 104 | raise CrabError('invalid finish status') 105 | 106 | self.store.log_finish(host, user, crabid, command, status, 107 | data.get('stdout'), data.get('stderr')) 108 | 109 | except CrabError as err: 110 | cherrypy.log.error('CrabError: log error: ' + str(err)) 111 | raise HTTPError(message='log error: ' + str(err)) 112 | 113 | def _read_json(self): 114 | """Attempts to interpret the HTTP PUT body as JSON and return 115 | the corresponding Python object. 116 | 117 | There could be a correpsonding _write_json method, but there 118 | is little need as the caller can just do: return json.dumps(...) 119 | and the CherryPy handler needs to pass the response back with 120 | return.""" 121 | 122 | message = latin_1_decode(cherrypy.request.body.read(), 'replace')[0] 123 | 124 | try: 125 | return json.loads(message) 126 | except ValueError: 127 | cherrypy.log.error('CrabError: Failed to read JSON: ' + message) 128 | raise HTTPError(400, message='Did not understand JSON') 129 | -------------------------------------------------------------------------------- /lib/crab/server/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2015-2018 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from logging.handlers import RotatingFileHandler 18 | import os 19 | import socket 20 | import sys 21 | 22 | from cherrypy.lib.reprconf import Config 23 | 24 | from crab.store.file import CrabStoreFile 25 | from crab.store.sqlite import CrabStoreSQLite 26 | 27 | 28 | def read_crabd_config(): 29 | """Determine Crab server configuration. 30 | 31 | This returns a CherryPy configuration dictionary, with 32 | values read from the config files and environment variables.""" 33 | 34 | config = Config() 35 | config.update({ 36 | 'global': { 37 | 'engine.autoreload.on': False, 38 | 'server.socket_port': 8000, 39 | 'server.socket_host': '0.0.0.0', 40 | }, 41 | 'crab': { 42 | 'home': os.path.join(sys.prefix, 'share', 'crab'), 43 | 'base_url': None, 44 | }, 45 | 'email': { 46 | 'server': 'mailhost', 47 | 'from': 'Crab Daemon', 48 | 'subject_ok': 'Crab notification', 49 | 'subject_warning': 'Crab notification (WARNING)', 50 | 'subject_error': 'Crab notification (ERROR)', 51 | }, 52 | 'notify': { 53 | 'timezone': 'UTC', 54 | 'daily': '0 0 * * *', 55 | }, 56 | 'store': { 57 | 'type': 'sqlite', 58 | 'file': '/var/lib/crab/crab.db', 59 | }, 60 | 'access_log': { 61 | 'max_size': 10, 62 | 'backup_count': 10, 63 | }, 64 | 'error_log': { 65 | 'max_size': 10, 66 | 'backup_count': 10, 67 | }, 68 | }) 69 | 70 | env = os.environ 71 | sysconfdir = env.get('CRABSYSCONFIG', '/etc/crab') 72 | userconfdir = env.get('CRABUSERCONFIG', os.path.expanduser('~/.crab')) 73 | 74 | try: 75 | config.update(os.path.join(sysconfdir, 'crabd.ini')) 76 | except IOError: 77 | pass 78 | 79 | try: 80 | config.update(os.path.join(userconfdir, 'crabd.ini')) 81 | except IOError: 82 | pass 83 | 84 | if 'CRABHOME' in env: 85 | config['crab']['home'] = env['CRABHOME'] 86 | 87 | config['/res'] = { 88 | 'tools.staticdir.on': True, 89 | 'tools.staticdir.dir': config['crab']['home'] + '/res', 90 | 'tools.staticdir.content_types': { 91 | 'css': 'text/css', 92 | 'eot': 'application/vnd.ms-fontobject', 93 | 'js': 'application/javascript', 94 | 'png': 'image/png', 95 | 'svg': 'image/svg+xml', 96 | 'ttf': 'application/x-font-ttf', 97 | 'woff': 'application/font-woff', 98 | 'woff2': 'font/woff2', 99 | }, 100 | } 101 | 102 | if 'base_url' not in config['crab'] or config['crab']['base_url'] is None: 103 | config['crab']['base_url'] = ( 104 | 'http://' + socket.getfqdn() + ':' + 105 | str(config['global']['server.socket_port'])) 106 | 107 | return config 108 | 109 | 110 | def construct_log_handler(filename, log_config): 111 | return RotatingFileHandler( 112 | filename, 113 | maxBytes=log_config['max_size'] * 1024 * 1024, 114 | backupCount=log_config['backup_count']) 115 | 116 | 117 | def construct_store(storeconfig, outputstore=None): 118 | """Constructs a storage backend from the given dictionary.""" 119 | 120 | if storeconfig['type'] == 'sqlite': 121 | store = CrabStoreSQLite(storeconfig['file'], outputstore) 122 | 123 | elif storeconfig['type'] == 'mysql': 124 | # Only import the MySQL store module when required in case the 125 | # mysql.connector module isn't installed. 126 | from crab.store.mysql import CrabStoreMySQL 127 | store = CrabStoreMySQL(host=storeconfig['host'], 128 | database=storeconfig['database'], 129 | user=storeconfig['user'], 130 | password=storeconfig['password'], 131 | outputstore=outputstore) 132 | 133 | elif storeconfig['type'] == 'file': 134 | store = CrabStoreFile(storeconfig['dir']) 135 | 136 | else: 137 | raise Exception('Unknown output store type: ' + storeconfig['type']) 138 | 139 | return store 140 | -------------------------------------------------------------------------------- /lib/crab/server/io.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 East Asian Observatory. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import json 17 | 18 | JOB_FIELDS = [ 19 | 'host', 20 | 'user', 21 | 'crabid', 22 | 'command', 23 | 'time', 24 | 'timezone', 25 | ] 26 | 27 | CONFIG_FIELDS = [ 28 | 'graceperiod', 29 | 'timeout', 30 | 'success_pattern', 31 | 'warning_pattern', 32 | 'fail_pattern', 33 | 'note', 34 | '*inhibit', 35 | ] 36 | 37 | NOTIFICATION_FIELDS = [ 38 | 'method', 39 | 'address', 40 | 'time', 41 | 'timezone', 42 | '*skip_ok', 43 | '*skip_warning', 44 | '*skip_error', 45 | '*include_output', 46 | ] 47 | 48 | 49 | def import_config(store, file_): 50 | """Read job and configuration information from a JSON file.""" 51 | 52 | # Read JSON from the given file handle. 53 | data = json.load(file_) 54 | 55 | # Ensure each job listed is present and update its configuration. 56 | for job in data['jobs']: 57 | id_ = store.check_job(**job['info']) 58 | 59 | configid = None 60 | if job['config'] is not None: 61 | configid = store.write_job_config(id_, **job['config']) 62 | 63 | # If there were notifications, try to fetch the existing notifications 64 | # so that those which match can be updated instead of being duplicated. 65 | # If we don't already have a configuration, create a blank one for 66 | # attaching notifications. 67 | if job['notifications']: 68 | # If we didn't set a config, check one didn't already exist. 69 | if configid is None: 70 | config = store.get_job_config(id_) 71 | if config is not None: 72 | configid = config['configid'] 73 | 74 | # If we still don't have a configid (didn't set and didn't already 75 | # exist) create one, otherwise fetch notifications. 76 | existing_notify = {} 77 | if configid is None: 78 | configid = store.write_job_config(id_) 79 | else: 80 | for notification in store.get_job_notifications(configid): 81 | existing_notify[_notify_key(notification)] = \ 82 | notification['notifyid'] 83 | 84 | for notification in job['notifications']: 85 | notifyid = existing_notify.get(_notify_key(notification)) 86 | store.write_notification(notifyid=notifyid, configid=configid, 87 | host=None, user=None, **notification) 88 | 89 | # Store any crontabs which were given. 90 | for crontab in data['crontabs']: 91 | store.write_raw_crontab(**crontab) 92 | 93 | # Get a list of existing "match" notifications, then store/update 94 | # those given. 95 | existing_notify = {} 96 | for notification in store.get_match_notifications(): 97 | existing_notify[_notify_key(notification, match=True)] = \ 98 | notification['notifyid'] 99 | 100 | for notification in data['notifications']: 101 | notifyid = existing_notify.get(_notify_key(notification, match=True)) 102 | store.write_notification(notifyid=notifyid, configid=None, 103 | **notification) 104 | 105 | 106 | def export_config(store, file_): 107 | """Write job and configuration information to a JSON file.""" 108 | 109 | # Create list of jobs. 110 | jobs = [] 111 | hostuser = set() 112 | for job in store.get_jobs(): 113 | hostuser.add((job['host'], job['user'])) 114 | 115 | config = store.get_job_config(job['id']) 116 | 117 | # If there was a configuration, also check for notifications. 118 | notifications = [] 119 | if config is not None: 120 | for notification in store.get_job_notifications( 121 | config['configid']): 122 | notifications.append(_filter_dict( 123 | notification, NOTIFICATION_FIELDS)) 124 | 125 | jobs.append({ 126 | 'info': _filter_dict(job, JOB_FIELDS), 127 | 'config': _filter_dict(config, CONFIG_FIELDS), 128 | 'notifications': notifications, 129 | }) 130 | 131 | # Retrieve raw crontabs. 132 | crontabs = [] 133 | for (host, user) in sorted(hostuser): 134 | crontab = store.get_raw_crontab(host, user) 135 | 136 | if crontab is not None: 137 | crontabs.append({ 138 | 'host': host, 139 | 'user': user, 140 | 'crontab': crontab, 141 | }) 142 | 143 | # Retrieve "match" notifications. 144 | notifications = [] 145 | for notification in store.get_match_notifications(): 146 | notifications.append(_filter_dict( 147 | notification, ['host', 'user'] + NOTIFICATION_FIELDS)) 148 | 149 | # Finally write the JSON to the given file handle. 150 | json.dump({ 151 | 'jobs': jobs, 152 | 'notifications': notifications, 153 | 'crontabs': crontabs, 154 | }, file_, indent=4, separators=(',', ': '), sort_keys=True) 155 | 156 | 157 | def _filter_dict(d, keys): 158 | """Filters a dictionary to contain only the given keys. 159 | 160 | If the input dictionary is None, None is returned. 161 | 162 | If a key is prefixed with a *, it is forced to be a boolean. 163 | """ 164 | 165 | if d is None: 166 | return d 167 | 168 | return dict( 169 | (key[1:], bool(d[key[1:]])) if key.startswith('*') else (key, d[key]) 170 | for key in keys) 171 | 172 | 173 | def _notify_key(notification, match=False): 174 | """Return notification information tuple for use in identifying a 175 | notification. 176 | 177 | If "match" is selected, then includes host and user for a match-type 178 | notification.""" 179 | 180 | if match: 181 | return (notification['host'], notification['user']) + \ 182 | _notify_key(notification) 183 | 184 | return (notification['method'], notification['address'], 185 | notification['time'], notification['timezone']) 186 | -------------------------------------------------------------------------------- /lib/crab/service/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # Copyright (C) 2016 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from datetime import datetime, timedelta 18 | from logging import getLogger 19 | import pytz 20 | import time 21 | from threading import Thread 22 | 23 | logger = getLogger(__name__) 24 | 25 | 26 | class CrabMinutely(Thread): 27 | """A thread which will call its run_minutely method for each minute 28 | which passes. 29 | 30 | The problem which this class seeks to address is that other methods of 31 | pausing a program (eg. threading.Timer, time.sleep) can't guarantee not 32 | to pause for longer than expected. Therefore in the context of 33 | cron jobs, it might be possible to miss a cron scheduling point.""" 34 | 35 | def __init__(self): 36 | """Constructor for minutely scheduled sevices. 37 | 38 | In order to allow subclasses to override the run method, 39 | we record the start time here.""" 40 | 41 | Thread.__init__(self) 42 | self._previous = datetime.now(pytz.UTC) 43 | 44 | def run(self): 45 | """Thread run function. 46 | 47 | This calls _check_minute on regular intervals.""" 48 | 49 | while True: 50 | time.sleep(5) 51 | self._check_minute() 52 | 53 | def _check_minute(self): 54 | """Check whether one or more minutes has passed, and if so, 55 | run the run_minutely method for each of them. 56 | 57 | If a subclass needs to implements its own run method, it should 58 | call this method regularly.""" 59 | 60 | delta = timedelta(seconds=55) 61 | current = datetime.now(pytz.UTC) 62 | previous = self._previous 63 | 64 | while minute_before(previous, current): 65 | previous = self._previous + delta 66 | 67 | if not minute_equal(previous, self._previous): 68 | try: 69 | self.run_minutely(previous.replace(second=0, 70 | microsecond=0)) 71 | except Exception as e: 72 | logger.exception("Error: run_minutely raised exception") 73 | 74 | self._previous = previous 75 | 76 | def run_minutely(self, datetime_): 77 | """This is the method which will be called each minute. It should 78 | be overridden by subclasses. 79 | 80 | Will be called with a datetime object for the relevant minute 81 | with the seconds and microseconds set to zero.""" 82 | 83 | pass 84 | 85 | 86 | def minute_equal(a, b): 87 | """Determine whether one time is in the same minute as another.""" 88 | return a.timetuple()[0:5] == b.timetuple()[0:5] 89 | 90 | 91 | def minute_before(a, b): 92 | """Determine whether one time is in a minute before another.""" 93 | return a.timetuple()[0:5] < b.timetuple()[0:5] 94 | -------------------------------------------------------------------------------- /lib/crab/service/clean.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 East Asian Observatory. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from datetime import timedelta 17 | 18 | from crab import CrabError 19 | from crab.service import CrabMinutely 20 | from crab.util.schedule import CrabSchedule 21 | 22 | 23 | class CrabCleanService(CrabMinutely): 24 | """Service to clean the store by removing old events.""" 25 | 26 | def __init__(self, config, store): 27 | """Constructor method. 28 | 29 | Stores the store object and a CrabSchedule object.""" 30 | 31 | CrabMinutely.__init__(self) 32 | 33 | self.store = store 34 | self.schedule = CrabSchedule(config['schedule'], config['timezone']) 35 | self.keep_days = config['keep_days'] 36 | 37 | def run_minutely(self, datetime_): 38 | """Performs cleaning if scheduled for the given minute.""" 39 | 40 | if self.schedule.match(datetime_): 41 | self.store.delete_old_events( 42 | datetime_=(datetime_ - timedelta(days=self.keep_days))) 43 | -------------------------------------------------------------------------------- /lib/crab/service/notify.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2015 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from logging import getLogger 18 | 19 | from crab import CrabError 20 | from crab.notify import CrabNotify, CrabNotifyJob 21 | from crab.service import CrabMinutely 22 | from crab.util.schedule import CrabSchedule 23 | 24 | logger = getLogger(__name__) 25 | 26 | 27 | class CrabNotifyService(CrabMinutely): 28 | """Service to send notifications as required. 29 | 30 | Currently only a single daily schedule is implemented.""" 31 | 32 | def __init__(self, config, store, notify): 33 | """Constructor method. 34 | 35 | Stores CrabNotify object and daily CrabSchedule object.""" 36 | 37 | CrabMinutely.__init__(self) 38 | 39 | self.store = store 40 | self.notify = notify 41 | self.schedule = CrabSchedule(config['daily'], 42 | config['timezone']) 43 | self.config = {} 44 | self.sched = {} 45 | 46 | def run_minutely(self, datetime_): 47 | """Issues notifications if any are scheduled for the given minute.""" 48 | 49 | current = [] 50 | match_daily = self.schedule.match(datetime_) 51 | 52 | if match_daily: 53 | daily_start = self.schedule.previous_datetime(datetime_) 54 | else: 55 | daily_start = None 56 | 57 | try: 58 | notifications = self.store.get_notifications() 59 | 60 | except CrabError as err: 61 | logger.exception('Error fetching notifications') 62 | return 63 | 64 | for notification in notifications: 65 | n_id = notification['notifyid'] 66 | 67 | if (n_id in self.config and n_id in self.sched and 68 | notification['time'] == self.config[n_id]['time'] and 69 | notification['timezone'] == self.config[n_id]['timezone']): 70 | schedule = self.sched[n_id] 71 | else: 72 | self.config[n_id] = notification 73 | if notification['time'] is not None: 74 | try: 75 | schedule = CrabSchedule(notification['time'], 76 | notification['timezone']) 77 | except CrabError as err: 78 | schedule = None 79 | logger.exception( 80 | 'Warning: could not read notification schedule') 81 | else: 82 | schedule = None 83 | 84 | self.sched[n_id] = schedule 85 | 86 | if schedule is None: 87 | if match_daily: 88 | current.append(CrabNotifyJob( 89 | notification, daily_start, datetime_)) 90 | else: 91 | if schedule.match(datetime_): 92 | current.append(CrabNotifyJob( 93 | notification, schedule.previous_datetime(datetime_), 94 | datetime_)) 95 | 96 | if current: 97 | self.notify(current) 98 | -------------------------------------------------------------------------------- /lib/crab/store/file.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import errno 17 | import os 18 | 19 | from crab import CrabError 20 | from crab.util.string import alphanum 21 | 22 | 23 | class CrabStoreFile: 24 | """Store class for cron job output. 25 | 26 | This backend currently implements only the write_job_output and 27 | get_job_output methods, to allow it to be used as an 28 | "outputstore" along with CrabStoreDB.""" 29 | 30 | def __init__(self, dir): 31 | """Constructor for file-based storage backend. 32 | 33 | Takes a path to the base directory in which the files are to be 34 | stored.""" 35 | 36 | self.dir = dir 37 | self.breakdigits = 3 38 | self.outext = 'txt' 39 | self.errext = 'err' 40 | self.tabext = 'txt' 41 | 42 | if not os.path.isdir(self.dir): 43 | raise CrabError('file store error: invalid base directory') 44 | 45 | if not os.access(self.dir, os.W_OK): 46 | raise CrabError('file store error: unwritable base directory') 47 | 48 | self.outputdir = os.path.join(dir, 'output') 49 | self.tabdir = os.path.join(dir, 'crontab') 50 | 51 | for directory in [self.outputdir, self.tabdir]: 52 | if not os.path.exists(directory): 53 | try: 54 | os.mkdir(directory) 55 | except OSError as err: 56 | if err.errno != errno.EEXIST: 57 | raise CrabError( 58 | 'file store error: ' 59 | 'could not make directory ' + directory + 60 | ': ' + str(err)) 61 | 62 | def write_job_output(self, finishid, host, user, id_, crabid, 63 | stdout, stderr): 64 | """Write the cron job output to a file. 65 | 66 | The only parameter required to uniquely identify the event 67 | associated with this output is the "finishid", but the 68 | host, user and job identifiers are also provided to allow 69 | hierarchical storage. 70 | 71 | Only writes a stdout file (extension set by self.outext, by default 72 | txt), and a stderr file (extension self.errext, default err) 73 | if they are not empty.""" 74 | 75 | path = self._make_output_path(finishid, host, user, id_, crabid) 76 | 77 | (dir, file) = os.path.split(path) 78 | 79 | if not os.path.exists(dir): 80 | try: 81 | os.makedirs(dir) 82 | except OSError as err: 83 | if err.errno != errno.EEXIST: 84 | raise CrabError( 85 | 'file store error: could not make directory: ' + 86 | str(err)) 87 | 88 | outfile = path + '.' + self.outext 89 | errfile = path + '.' + self.errext 90 | 91 | if os.path.exists(outfile) or os.path.exists(errfile): 92 | raise CrabError('file store error: file already exists: ' + path) 93 | 94 | try: 95 | if stdout: 96 | with open(outfile, 'w') as file: 97 | file.write(stdout) 98 | 99 | if stderr: 100 | with open(errfile, 'w') as file: 101 | file.write(stderr) 102 | 103 | except IOError as err: 104 | raise CrabError('file store error: could not write files: ' + 105 | str(err)) 106 | 107 | def get_job_output(self, finishid, host, user, id_, crabid): 108 | """Find the file containing the cron job output and read it. 109 | 110 | As for write_job_output, only the "finishid" is logically required, 111 | but this method makes use of the host, user and job identifiers 112 | to read from a directory hierarchy. 113 | 114 | Requires there to be an stdout file but allows the 115 | stderr file to be absent.""" 116 | 117 | path = self._make_output_path(finishid, host, user, id_, crabid) 118 | outfile = path + '.' + self.outext 119 | errfile = path + '.' + self.errext 120 | 121 | if not (os.path.exists(outfile) or os.path.exists(errfile)): 122 | if crabid is not None: 123 | # Try again with no crabid. This is to handle the case where 124 | # a job is imported with no name, but is subsequently named. 125 | path = self._make_output_path(finishid, host, user, id_, None) 126 | outfile = path + '.' + self.outext 127 | errfile = path + '.' + self.errext 128 | 129 | else: 130 | # Return now just to avoid testing the same files again. 131 | return ('', '') 132 | 133 | try: 134 | if os.path.exists(outfile): 135 | with open(outfile) as file: 136 | stdout = file.read() 137 | else: 138 | stdout = '' 139 | 140 | if os.path.exists(errfile): 141 | with open(errfile) as file: 142 | stderr = file.read() 143 | else: 144 | stderr = '' 145 | 146 | except IOError as err: 147 | raise CrabError('file store error: could not read files: ' + 148 | str(err)) 149 | 150 | return (stdout, stderr) 151 | 152 | def write_raw_crontab(self, host, user, crontab): 153 | """Writes the given crontab to a file.""" 154 | 155 | pathname = self._make_crontab_path(host, user) 156 | 157 | (dir, file) = os.path.split(pathname) 158 | 159 | if not os.path.exists(dir): 160 | try: 161 | os.makedirs(dir) 162 | except OSError as err: 163 | if err.errno != errno.EEXIST: 164 | raise CrabError( 165 | 'file store error: could not make directory: ' + 166 | str(err)) 167 | 168 | try: 169 | with open(pathname, 'w') as file: 170 | file.write('\n'.join(crontab)) 171 | 172 | except IOError as err: 173 | raise CrabError('file store error: could not write crontab: ' + 174 | str(err)) 175 | 176 | def get_raw_crontab(self, host, user): 177 | """Reads the given user's crontab from a file.""" 178 | 179 | pathname = self._make_crontab_path(host, user) 180 | 181 | if not os.path.exists(pathname): 182 | return None 183 | 184 | try: 185 | with open(pathname) as file: 186 | crontab = file.read() 187 | 188 | except IOError as err: 189 | raise CrabError('file store error: could not read crontab: ' + 190 | str(err)) 191 | 192 | return crontab.split('\n') 193 | 194 | def _make_output_path(self, finishid, host, user, id_, crabid): 195 | """Determine the full path to use to store output 196 | (excluding file extensions). 197 | 198 | This uses a directory heirarchy: 199 | 200 | * host 201 | * user 202 | * crabid (name) or ID (number) 203 | * finish ID 204 | 205 | Where the finish ID is broken into blocks of a few characters, 206 | the first of which is zero-padded to ensure all blocks are the 207 | same length. This prevents an excessive number of entries 208 | being placed in a single directory, while the path is easily 209 | determined without needing to read the directory structure. 210 | So breaking on the default number of digits (3) finish ID 1 would 211 | yield 001 whereas 2005 would yield 002/005.""" 212 | 213 | if crabid is None or crabid == '': 214 | job = str(id_) 215 | else: 216 | job = alphanum(crabid) 217 | 218 | finish = str(finishid) 219 | finishpath = [] 220 | 221 | lead = len(finish) % self.breakdigits 222 | if lead: 223 | finishpath.append('0' * (self.breakdigits - lead) + finish[:lead]) 224 | finish = finish[lead:] 225 | 226 | finishpath.extend([finish[x:x + self.breakdigits] 227 | for x in range(0, len(finish), self.breakdigits)]) 228 | 229 | return os.path.join(self.outputdir, alphanum(host), alphanum(user), 230 | job, *finishpath) 231 | 232 | def _make_crontab_path(self, host, user): 233 | """Determine the full path to be used to store a crontab.""" 234 | 235 | return (os.path.join(self.tabdir, alphanum(host), alphanum(user)) + 236 | '.' + self.tabext) 237 | -------------------------------------------------------------------------------- /lib/crab/store/mysql.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015-2016 East Asian Observatory. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from __future__ import absolute_import 17 | 18 | import re 19 | import pytz 20 | 21 | import mysql.connector 22 | from mysql.connector.errors import Error as _MySQLError 23 | from mysql.connector.cursor import MySQLCursor 24 | 25 | from crab.store.db import CrabStoreDB, CrabDBLock 26 | 27 | 28 | class CrabStoreMySQLCursor(MySQLCursor): 29 | """MySQL compatability cursor class.""" 30 | 31 | def execute(self, query, params): 32 | """Execute an SQL query. 33 | 34 | This method prepares the query for use with MySQL and then 35 | calls the (superclass) MySQLCursor.execute method. 36 | 37 | This is for compatability with SQL statements which were 38 | written for SQLite.""" 39 | 40 | # Replace placeholders. 41 | query = re.sub('\?', '%s', query) 42 | 43 | # Remove column type instructions. 44 | query = re.sub('AS "([a-z]+) \[timestamp\]"', '', query) 45 | 46 | return MySQLCursor.execute(self, query, params) 47 | 48 | 49 | class CrabStoreMySQL(CrabStoreDB): 50 | """MySQL-based storage class.""" 51 | 52 | def __init__(self, host, database, user, password, outputstore=None): 53 | """Connects to MySQL and initializes the storage object.""" 54 | 55 | conn = mysql.connector.connect( 56 | host=host, database=database, user=user, password=password, 57 | time_zone='+00:00') 58 | 59 | CrabStoreDB.__init__( 60 | self, 61 | lock=CrabDBLock( 62 | conn, error_class=_MySQLError, 63 | cursor_args={'cursor_class': CrabStoreMySQLCursor}, 64 | ping=True), 65 | outputstore=outputstore) 66 | -------------------------------------------------------------------------------- /lib/crab/store/sqlite.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # Copyright (C) 2015-2016 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from contextlib import closing 18 | import os 19 | import pytz 20 | 21 | import sqlite3 22 | 23 | from crab.store.db import CrabStoreDB, CrabDBLock 24 | 25 | 26 | class CrabStoreSQLite(CrabStoreDB): 27 | def __init__(self, filename, outputstore=None): 28 | if filename != ':memory:' and not os.path.exists(filename): 29 | raise Exception('SQLite file does not exist') 30 | 31 | conn = sqlite3.connect( 32 | filename, check_same_thread=False, 33 | detect_types=sqlite3.PARSE_COLNAMES) 34 | 35 | with closing(conn.cursor()) as c: 36 | c.execute("PRAGMA foreign_keys = ON") 37 | 38 | CrabStoreDB.__init__( 39 | self, 40 | lock=CrabDBLock(conn, error_class=sqlite3.DatabaseError), 41 | outputstore=outputstore) 42 | -------------------------------------------------------------------------------- /lib/crab/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/lib/crab/util/__init__.py -------------------------------------------------------------------------------- /lib/crab/util/bus.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 East Asian Observatory. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | def priority(level): 18 | """Decorator to set the priority attribute of a function.""" 19 | 20 | def decorator(f): 21 | f.priority = level 22 | return f 23 | return decorator 24 | 25 | 26 | class CrabStoreListener: 27 | """Base class for plugins which require a store. 28 | 29 | Listens on the "crab-store" channel, setting the instance value "store" 30 | to the store object received.""" 31 | 32 | def __init__(self, bus): 33 | self.bus = bus 34 | self.store = None 35 | 36 | def subscribe(self): 37 | self.bus.subscribe('crab-store', self.__store) 38 | 39 | def __store(self, store): 40 | self.store = store 41 | 42 | 43 | class CrabPlugin(CrabStoreListener): 44 | """Class to launch Crab services as CherryPy plugins. 45 | 46 | This class subscribes to the "start" channel. When it recieves a message, 47 | it constructs an instance of the service class and publishes it on the 48 | "crab-service" channel.""" 49 | 50 | def __init__(self, bus, name, class_, **kwargs): 51 | super(CrabPlugin, self).__init__(bus) 52 | 53 | self.name = name 54 | self.class_ = class_ 55 | self.kwargs = kwargs 56 | 57 | def subscribe(self): 58 | super(CrabPlugin, self).subscribe() 59 | 60 | self.bus.subscribe('start', self.start) 61 | 62 | # If the service takes a "notify" argument (specified as notify=None) 63 | # then also subscribe to the "crab-notify" channel. 64 | if self.kwargs.get('notify', ()) is None: 65 | self.bus.subscribe('crab-notify', self.notify) 66 | 67 | @priority(71) 68 | def start(self): 69 | self.bus.log('Starting Crab service "{}"'.format(self.name)) 70 | service = self.class_(store=self.store, **self.kwargs) 71 | service.daemon = True 72 | service.start() 73 | 74 | self.bus.publish('crab-service', self.name, service) 75 | 76 | def notify(self, notify): 77 | self.kwargs['notify'] = notify 78 | -------------------------------------------------------------------------------- /lib/crab/util/compat.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2021 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import signal 18 | import sys 19 | 20 | 21 | def restore_signals(): 22 | """Restore signals which Python otherwise ignores. 23 | 24 | For more information about this issue, please see: 25 | http://bugs.python.org/issue1652""" 26 | 27 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) 28 | signal.signal(signal.SIGXFSZ, signal.SIG_DFL) 29 | 30 | 31 | # Determine which options should be given to the subprocess module 32 | # when starting new processes. The "restore_signals" option was 33 | # added in Python 3.2, so we need only ensure that it is turned on. 34 | # Otherwise if the backported subprocess32 module is not available, 35 | # we need to provide a function to do this and a dummy timeout implementation. 36 | have_new_subprocess = True 37 | if sys.version_info >= (3, 2): 38 | import subprocess 39 | from subprocess import TimeoutExpired 40 | 41 | else: 42 | try: 43 | import subprocess32 as subprocess 44 | from subprocess32 import TimeoutExpired 45 | 46 | except ImportError: 47 | have_new_subprocess = False 48 | import subprocess 49 | 50 | 51 | if have_new_subprocess: 52 | subprocess_options = {'restore_signals': True} 53 | 54 | def subprocess_communicate(p, input=None, timeout=None): 55 | return p.communicate(input=input, timeout=timeout) 56 | 57 | def subprocess_call(args, timeout=None, **kwargs): 58 | return subprocess.call(args, timeout=timeout, **kwargs) 59 | 60 | else: 61 | subprocess_options = {'preexec_fn': restore_signals} 62 | 63 | def subprocess_communicate(p, input=None, timeout=None): 64 | return p.communicate(input=input) 65 | 66 | def subprocess_call(args, timeout=None, **kwargs): 67 | return subprocess.call(args, **kwargs) 68 | 69 | class TimeoutExpired(Exception): 70 | pass 71 | -------------------------------------------------------------------------------- /lib/crab/util/crontab.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # Copyright (C) 2015-2016 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | 19 | from crab.util.string import \ 20 | quote_multiword, remove_quotes, split_crab_vars, true_string 21 | 22 | # These patterns do not deal with quoting or trailing spaces, 23 | # so these must be dealt with in the parse_crontab function. 24 | blankline = re.compile('^\s*$') 25 | comment = re.compile('^\s*#') 26 | variable = re.compile('^\s*(\w+)\s*=\s*(.*)$') 27 | cronrule = re.compile('^\s*(@\w+|\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.*)$') 28 | plain_percent = re.compile('(?. 16 | 17 | from __future__ import absolute_import 18 | 19 | from datetime import datetime 20 | 21 | import pytz 22 | 23 | 24 | def parse_datetime(datetime_): 25 | """Parse a datetime string. 26 | 27 | The returned datetime object will include the UTC timezone.""" 28 | 29 | return datetime.strptime( 30 | datetime_, '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.UTC) 31 | 32 | 33 | def format_datetime(datetime_): 34 | """Converts a datetime into a string. 35 | 36 | Includes conversion to UTC.""" 37 | 38 | return datetime_.astimezone(pytz.UTC).strftime('%Y-%m-%d %H:%M:%S') 39 | -------------------------------------------------------------------------------- /lib/crab/util/filter.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2015 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import pytz 18 | 19 | from crab import CrabEvent, CrabStatus 20 | 21 | 22 | class CrabEventFilter: 23 | """Class implementing an event filtering action.""" 24 | 25 | default_timezone = pytz.UTC 26 | 27 | def __init__(self, store, timezone=None): 28 | """Construct filter object. 29 | 30 | Just stores the given information.""" 31 | 32 | self.store = store 33 | self.set_timezone(timezone) 34 | 35 | self.errors = None 36 | self.warnings = None 37 | 38 | def set_timezone(self, timezone): 39 | """Sets the timezone used by the filter.""" 40 | 41 | if timezone is None: 42 | self.zoneinfo = self.__class__.default_timezone 43 | else: 44 | try: 45 | self.zoneinfo = pytz.timezone(timezone) 46 | except pytz.UnknownTimeZoneError: 47 | self.zoneinfo = self.__class__.default_timezone 48 | 49 | @classmethod 50 | def set_default_timezone(cls, timezone): 51 | """Sets the default timezone for all filters.""" 52 | 53 | if timezone is None: 54 | cls.default_timezone = pytz.UTC 55 | else: 56 | try: 57 | cls.default_timezone = pytz.timezone(timezone) 58 | except pytz.UnknownTimeZoneError: 59 | cls.default_timezone = pytz.UTC 60 | 61 | def __call__(self, events, skip_ok=False, skip_warning=False, 62 | skip_error=False, skip_trivial=True, skip_start=False, 63 | squash_start=False): 64 | """Performs filtering, and returns the altered event list.""" 65 | 66 | output = [] 67 | squash = set() 68 | self.errors = 0 69 | self.warnings = 0 70 | 71 | for (i, e) in enumerate(events): 72 | if i in squash: 73 | continue 74 | 75 | e = e.copy() 76 | 77 | if e['type'] == CrabEvent.START: 78 | if skip_start: 79 | continue 80 | else: 81 | if (skip_trivial and CrabStatus.is_trivial(e['status']) or 82 | skip_ok and CrabStatus.is_ok(e['status']) or 83 | skip_warning and CrabStatus.is_warning(e['status']) or 84 | skip_error and CrabStatus.is_error(e['status'])): 85 | continue 86 | 87 | if CrabStatus.is_error(e['status']): 88 | self.errors += 1 89 | if CrabStatus.is_warning(e['status']): 90 | self.warnings += 1 91 | 92 | if squash_start and e['type'] == CrabEvent.FINISH: 93 | start = _find_previous_start(events, i) 94 | if start is not None: 95 | squash.add(start) 96 | delta = e['datetime'] - events[start]['datetime'] 97 | e['duration'] = str(delta) 98 | 99 | e['datetime'] = self.in_timezone(e['datetime']) 100 | 101 | output.append(e) 102 | 103 | return output 104 | 105 | def in_timezone(self, datetime_): 106 | """Convert the datetime string as output by the database 107 | to a string in the specified timezone. 108 | 109 | Includes the zone code to indicate that the conversion has been 110 | performed.""" 111 | 112 | if datetime_ is None: 113 | return None 114 | 115 | return datetime_.astimezone( 116 | self.zoneinfo).strftime('%Y-%m-%d %H:%M:%S %Z') 117 | 118 | 119 | def _find_previous_start(events, i): 120 | """Looks in the event list, past position i, for the previous start. 121 | 122 | Skips over alarms and other trivial events.""" 123 | 124 | i += 1 125 | 126 | while (i < len(events)): 127 | e = events[i] 128 | 129 | if e['type'] == CrabEvent.START: 130 | return i 131 | 132 | elif (e['type'] != CrabEvent.ALARM and 133 | not CrabStatus.is_trivial(e['status'])): 134 | return None 135 | 136 | i += 1 137 | 138 | return None 139 | -------------------------------------------------------------------------------- /lib/crab/util/guesstimezone.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # Copyright (C) 2016 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import pytz 19 | import re 20 | 21 | # There really ought to be a better way of doing this! You could read 22 | # /etc/sysconfig/clock but that would only work on certain systems. The 23 | # following might work anywhere the timezone database is installed in the 24 | # correct place. 25 | # 26 | # The Perl module DateTime::TimeZone::Local::Unix uses this method, among 27 | # others. TODO: implement some of the other methods. 28 | 29 | 30 | def guess_timezone(): 31 | """Function to try to determine the operating system's timezone setting. 32 | 33 | Currently this checks for a TZ environment variable. Otherwise 34 | it checks if /etc/localtime is a link or tries to find the file in 35 | /usr/share/zoneinfo which matches. It uses pytz to get a list of 36 | common timezones to try.""" 37 | 38 | if 'TZ' in os.environ: 39 | return os.environ['TZ'] 40 | 41 | # Before reading /etc/localtime, see if it is a symlink. 42 | try: 43 | link = os.readlink('/etc/localtime') 44 | m = re.search('/share/zoneinfo/([-_A-Za-z0-9/]+)$', link) 45 | if m: 46 | zone = m.group(1) 47 | if zone in pytz.all_timezones: 48 | return zone 49 | except: 50 | pass 51 | 52 | # Final method: read /etc/localtime and look for the same file in 53 | # /usr/share/zoneinfo/. 54 | try: 55 | f = open('/etc/localtime', 'rb') 56 | localtime = f.read() 57 | f.close() 58 | except: 59 | return None 60 | 61 | for zone in pytz.common_timezones: 62 | try: 63 | f = open('/usr/share/zoneinfo/' + zone, 'rb') 64 | timezone = f.read() 65 | f.close() 66 | 67 | if timezone == localtime: 68 | return zone 69 | 70 | except: 71 | pass 72 | 73 | return None 74 | -------------------------------------------------------------------------------- /lib/crab/util/pid.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Science and Technology Facilities Council. 2 | # Copyright (C) 2016 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # hashlib replaced md5 in Python 2.5 18 | try: 19 | from hashlib import md5 20 | except ImportError: 21 | from md5 import md5 22 | import os 23 | import os.path 24 | 25 | 26 | def pidfile_write(pidfile, pid): 27 | """Attempt to write a key for the given process into the file specified.""" 28 | 29 | f = None 30 | 31 | try: 32 | try: 33 | f = open(pidfile, 'w') 34 | f.write(_get_process_key(pid)) 35 | 36 | except IOError: 37 | pass 38 | 39 | finally: 40 | if f is not None: 41 | f.close() 42 | 43 | 44 | def pidfile_running(pidfile): 45 | """Read the pidfile specified and check if the process is running. 46 | 47 | If the pidfile was found then its timestamps are updated 48 | (using `os.utime`). This is to try to avoid the pidfile being 49 | automatically removed from temporary directories if a job 50 | runs for a very long time.""" 51 | 52 | f = None 53 | 54 | try: 55 | try: 56 | f = open(pidfile) 57 | return _check_process_key(f.read().strip()) 58 | 59 | except IOError: 60 | return False 61 | 62 | finally: 63 | if f is not None: 64 | f.close() 65 | 66 | try: 67 | os.utime(pidfile, None) 68 | except: 69 | pass 70 | 71 | 72 | def pidfile_delete(pidfile): 73 | """Attempt to delete the specified pidfile.""" 74 | 75 | try: 76 | os.unlink(pidfile) 77 | 78 | except OSError: 79 | pass 80 | 81 | 82 | def _get_process_key(pid): 83 | """Generate a string uniquely identifying a processess. 84 | 85 | If the cmdline file for this process can be read, then 86 | return a string containg the PID and the MD5 digest of that 87 | file. 88 | 89 | Otherwise just return a string containing the PID.""" 90 | 91 | if not isinstance(pid, int): 92 | raise Exception('Process ID is not an integer.') 93 | 94 | f = None 95 | 96 | try: 97 | try: 98 | f = open(os.path.join('/proc', str(pid), 'cmdline'), 'rb') 99 | h = md5(f.read(1024)) 100 | 101 | except IOError: 102 | h = None 103 | 104 | finally: 105 | if f is not None: 106 | f.close() 107 | 108 | if h is None: 109 | return str(pid) 110 | 111 | else: 112 | return str(pid) + ' ' + h.hexdigest() 113 | 114 | 115 | def _check_process_key(key): 116 | """Check a process key generated by _get_process_key(). 117 | 118 | If the key contains spaces, the PID is extracted from the 119 | first word and the key is compared to the output 120 | of _get_process_key(). 121 | 122 | Otherwise take the key to be a plain PID and check that the 123 | process is still running.""" 124 | 125 | if ' ' in key: 126 | (pid, hash) = key.split(' ') 127 | 128 | try: 129 | return _get_process_key(int(pid)) == key 130 | 131 | except ValueError: 132 | return False 133 | 134 | else: 135 | try: 136 | pid = int(key) 137 | 138 | except ValueError: 139 | return False 140 | 141 | try: 142 | os.kill(pid, 0) 143 | return True 144 | 145 | except OSError: 146 | return False 147 | -------------------------------------------------------------------------------- /lib/crab/util/schedule.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from __future__ import absolute_import 17 | 18 | from datetime import timedelta 19 | from logging import getLogger 20 | import pytz 21 | 22 | from crontab import CronTab 23 | 24 | from crab import CrabError 25 | 26 | logger = getLogger(__name__) 27 | 28 | 29 | class CrabSchedule(CronTab): 30 | """Class handling the schedule of a cron job.""" 31 | 32 | def __init__(self, specifier, timezone): 33 | """Construct a CrabSchedule object from a cron time specifier 34 | and the associated timezone name. 35 | 36 | The timezone string, if provided, is converted into an object 37 | using the pytz module.""" 38 | 39 | try: 40 | item = CronTab.__init__(self, specifier) 41 | 42 | except ValueError as err: 43 | raise CrabError('Failed to parse cron time specifier ' + 44 | specifier + ' reason: ' + str(err)) 45 | 46 | self.timezone = None 47 | 48 | if timezone is not None: 49 | try: 50 | # pytz returns the same object if called twice 51 | # with the same timezone, so we don't need to cache 52 | # the timezone objects by zone name. 53 | self.timezone = pytz.timezone(timezone) 54 | except pytz.UnknownTimeZoneError: 55 | logger.warning('Warning: unknown time zone {}'.format(timezone)) 56 | 57 | def match(self, datetime_): 58 | """Determines whether the given datetime matches the scheduling 59 | rules stored in the class instance. 60 | 61 | The datetime is converted to the stored timezone, and then the 62 | components of the time are checked against the matchers 63 | in the CronTab superclass.""" 64 | 65 | localtime = self._localtime(datetime_) 66 | 67 | return (self.matchers.minute(localtime.minute, localtime) and 68 | self.matchers.hour(localtime.hour, localtime) and 69 | self.matchers.day(localtime.day, localtime) and 70 | self.matchers.month(localtime.month, localtime) and 71 | self.matchers.weekday(localtime.isoweekday() % 7, localtime)) 72 | 73 | def next_datetime(self, datetime_): 74 | """return a datetime rather than number of 75 | seconds.""" 76 | 77 | localtime = self._localtime(datetime_) 78 | return datetime_ + timedelta(seconds=int(self.next(localtime))) 79 | 80 | def previous_datetime(self, datetime_): 81 | """return a datetime rather than number of 82 | seconds.""" 83 | 84 | localtime = self._localtime(datetime_) 85 | return datetime_ + timedelta(seconds=int(self.previous(localtime))) 86 | 87 | def _localtime(self, datetime_): 88 | if self.timezone is not None: 89 | return datetime_.astimezone(self.timezone) 90 | else: 91 | # Currently assume UTC. 92 | return datetime_ 93 | -------------------------------------------------------------------------------- /lib/crab/util/statuspattern.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import re 17 | 18 | from crab import CrabStatus 19 | 20 | 21 | def check_status_patterns(status, config, output): 22 | """Function to update a job status based on the patterns. 23 | 24 | Compares the given output with the patterns in the 25 | job configuration, and returns the updated status.""" 26 | 27 | # Is this a special status which doesn't indicate job completion? 28 | # If so we should not attempt to look at the patterns. 29 | if status == CrabStatus.ALREADYRUNNING: 30 | return status 31 | 32 | # Check for error status. 33 | if CrabStatus.is_error(status): 34 | return status 35 | 36 | fail_pattern = config['fail_pattern'] 37 | if fail_pattern is not None and re.search(fail_pattern, output): 38 | return CrabStatus.FAIL 39 | 40 | # Check for warning status. 41 | if CrabStatus.is_warning(status): 42 | return status 43 | 44 | warning_pattern = config['warning_pattern'] 45 | if warning_pattern is not None and re.search(warning_pattern, output): 46 | return CrabStatus.WARNING 47 | 48 | # Check for good status. 49 | success_pattern = config['success_pattern'] 50 | if success_pattern is not None and re.search(success_pattern, output): 51 | return CrabStatus.SUCCESS 52 | 53 | # No match -- decide what to do based on which patterns were defined. 54 | if success_pattern is not None: 55 | if fail_pattern is not None: 56 | # There were success and fail patterns but we matched neither 57 | # of them, so the status is UNKNOWN. 58 | return CrabStatus.UNKNOWN 59 | else: 60 | # There was a success pattern which we did not match, so 61 | # assume this was a failure as there was no explicit success 62 | # match. 63 | return CrabStatus.FAIL 64 | 65 | # Otherwise return the original status. If there was a failure 66 | # pattern, then we already know we didn't match it. 67 | return status 68 | -------------------------------------------------------------------------------- /lib/crab/util/string.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # Copyright (C) 2015 East Asian Observatory. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | 19 | 20 | def remove_quotes(value): 21 | """If the given string starts and ends with matching quote marks, 22 | remove them from the returned value. 23 | 24 | >>> remove_quotes('alpha') 25 | 'alpha' 26 | >>> remove_quotes('"bravo"') 27 | 'bravo' 28 | >>> remove_quotes("'charlie'") 29 | 'charlie' 30 | 31 | If the quotes are mismatched it should not remove them. 32 | 33 | >>> remove_quotes('"delta') 34 | '"delta' 35 | >>> remove_quotes("echo'") 36 | "echo'" 37 | """ 38 | 39 | if ((value.startswith("'") and value.endswith("'")) or 40 | (value.startswith('"') and value.endswith('"'))): 41 | return value[1:-1] 42 | else: 43 | return value 44 | 45 | 46 | def quote_multiword(value): 47 | """If the given string contains space characters, return it 48 | surrounded by double quotes, otherwise return the original string. 49 | 50 | >>> quote_multiword('alpha') 51 | 'alpha' 52 | >>> quote_multiword('bravo charlie') 53 | '"bravo charlie"' 54 | """ 55 | 56 | if value.find(' ') != -1: 57 | return '"' + value + '"' 58 | else: 59 | return value 60 | 61 | 62 | def split_quoted_word(value): 63 | """Splits the given string on the first word boundary, unless it starts 64 | with a quote. 65 | 66 | If quotes are present it splits at the first matching quote. Eg.: 67 | 68 | >>> split_quoted_word('alpha bravo charlie delta echo') 69 | ['alpha', 'bravo charlie delta echo'] 70 | >>> split_quoted_word('"alpha bravo" charlie delta echo') 71 | ('alpha bravo', 'charlie delta echo') 72 | 73 | Does not handle escaped quotes within the string.""" 74 | 75 | if value.startswith("'"): 76 | (a, b) = value[1:].split("'", 1) 77 | elif value.startswith('"'): 78 | (a, b) = value[1:].split('"', 1) 79 | else: 80 | return value.split(None, 1) 81 | 82 | return (a, b.lstrip()) 83 | 84 | 85 | def split_crab_vars(command): 86 | """Looks for Crab environment variables at the start of a command. 87 | 88 | Bourne-style shells allow environment variables to be specified 89 | at the start of a command. This function takes a string corresponding 90 | to a command line to be executed by a shell, and looks for environment 91 | variables in the 'CRAB namespace', i.e. those consisting of CRAB 92 | followed by a number of upper case characters. 93 | 94 | >>> split_crab_vars('some command') 95 | ('some command', {}) 96 | >>> split_crab_vars('CRABALPHA=bravo another command') 97 | ('another command', {'CRABALPHA': 'bravo'}) 98 | 99 | Returns: a tuple consisting of the remainder of the command and 100 | a dictionary of Crab's environment variables.""" 101 | 102 | crabvar = re.compile('^(CRAB[A-Z]+)=') 103 | vars = {} 104 | 105 | while True: 106 | m = crabvar.match(command) 107 | 108 | if m is None: 109 | break 110 | 111 | (value, command) = split_quoted_word(command[m.end():]) 112 | vars[m.group(1)] = value 113 | 114 | return (command, vars) 115 | 116 | 117 | def alphanum(value): 118 | """Removes all non-alphanumeric characters from the string, 119 | replacing them with underscores. 120 | 121 | >>> alphanum('a3.s_?x9!t') 122 | 'a3_s__x9_t' 123 | """ 124 | 125 | return re.sub('[^a-zA-Z0-9]', '_', value) 126 | 127 | 128 | def mergelines(text): 129 | """Merges the lines of a string by removing newline characters. 130 | 131 | >>> mergelines('alpha' + chr(10) + 'bravo') 132 | 'alphabravo' 133 | """ 134 | 135 | output = '' 136 | for line in text.split('\n'): 137 | output = output + line.strip() 138 | return output 139 | 140 | 141 | def true_string(text): 142 | """Tests whether the string represents a true value. 143 | 144 | The following strings are false: 145 | 146 | >>> [true_string(x) for x in ['0', 'no', 'false', 'off']] 147 | [False, False, False, False] 148 | 149 | And anything else is taken to be true, for example: 150 | 151 | >>> [true_string(x) for x in ['1', 'yes', 'true', 'on']] 152 | [True, True, True, True] 153 | 154 | The results are case insensitive. 155 | 156 | >>> [true_string(x) for x in ['no', 'NO', 'True', 'Off']] 157 | [False, False, True, False] 158 | """ 159 | 160 | return text.lower() not in ['0', 'no', 'false', 'off'] 161 | -------------------------------------------------------------------------------- /lib/crab/util/web.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Science and Technology Facilities Council. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import markupsafe 17 | 18 | 19 | def abbr(text, limit=60, tolerance=10): 20 | """Returns an abbreviated and HTML-escaped version of the specified text. 21 | 22 | The text is trimmed to the given length limit, but if a space is found 23 | within the preceeding 'tolerance' number of characters, then it 24 | is trimmed there. The result is an HTML span element with the 25 | full text as the title, unless it was not necessary to trim it. 26 | 27 | >>> abbr('alpha bravo', 15, 5) 28 | 'alpha bravo' 29 | >>> abbr('alpha bravo charlie', 15, 5) 30 | 'alpha bravo…' 31 | """ 32 | 33 | if len(text) > limit: 34 | space = text.rfind(' ', limit - tolerance, limit) 35 | if space == -1: 36 | shorttext = text[:limit] 37 | else: 38 | shorttext = text[:space] 39 | 40 | return ('' + str(markupsafe.escape(shorttext)) + 42 | '…') 43 | 44 | else: 45 | return str(markupsafe.escape(text)) 46 | -------------------------------------------------------------------------------- /lib/crab/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 East Asian Observatory. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | version = '0.5.1' 17 | -------------------------------------------------------------------------------- /lib/crab/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/lib/crab/web/__init__.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | crontab>=0.15 2 | CherryPy 3 | Mako 4 | pytz 5 | -------------------------------------------------------------------------------- /res/coloroutput.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | // Check for an ANSI converter function. 3 | var ansi_to_html; 4 | 5 | if (typeof ansi_up !== 'undefined') { 6 | ansi_to_html = ansi_up.ansi_to_html; 7 | } 8 | 9 | // If we have an ANSI converter and the output appears 10 | // to include escape sequences, add a control which 11 | // can be used to run the output through the converter. 12 | if (typeof ansi_to_html !== 'undefined') { 13 | $('.joboutput').each(function (index) { 14 | var joboutput = $(this); 15 | if (joboutput.html().search(/\033\[/) != -1) { 16 | var colorinvert = 0; 17 | var colorpara = $.parseHTML('

'); 18 | var invertpara = $.parseHTML(''); 19 | var colorlink = $.parseHTML( 20 | ' Display ANSI colors.'); 21 | var invertlink = $.parseHTML( 22 | ' Invert colors.'); 23 | joboutput.before(colorpara); 24 | joboutput.before(invertpara); 25 | $(colorpara).append(colorlink); 26 | $(invertpara).append(invertlink); 27 | $(colorlink).click(function (event) { 28 | $(colorpara).hide(); 29 | $(invertpara).show(); 30 | joboutput.html(ansi_to_html(joboutput.html())); 31 | event.preventDefault(); 32 | }); 33 | $(invertlink).click(function (event) { 34 | colorinvert = colorinvert ? 0 : 1; 35 | var fg = colorinvert ? 'white' : 'black'; 36 | var bg = colorinvert ? 'black' : 'white'; 37 | joboutput.parent().css('color', fg); 38 | joboutput.parent().css('background-color', bg); 39 | event.preventDefault(); 40 | }); 41 | } 42 | }); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /res/crab.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: silver; 3 | font-family: sans-serif; 4 | font-size: 12pt; 5 | text-align: center; 6 | margin: 0px; 7 | padding: 0px; 8 | } 9 | 10 | div#content { 11 | display: inline-block; 12 | text-align: left; 13 | min-width: 400px; 14 | margin-top: 40px; 15 | margin-bottom: 40px; 16 | margin-left: auto; 17 | margin-right: auto; 18 | background: white; 19 | box-shadow: 0px 0px 30px #002; 20 | border-radius: 10px; 21 | padding: 20px; 22 | border: 2px solid #00A; 23 | clear: both; 24 | } 25 | 26 | div#headerbar { 27 | background: grey; 28 | margin: 0px; 29 | padding: 0px; 30 | border-bottom: 2px solid #00A; 31 | box-shadow: 0px 0px 30px #002; 32 | text-align: left; 33 | width: 100%; 34 | float: left; 35 | } 36 | 37 | div#headerbar div { 38 | margin: 0px; 39 | padding: 10px; 40 | float: right; 41 | text-align: right; 42 | } 43 | 44 | div#headerbar div:before { 45 | content: "."; 46 | font-size: 24pt; 47 | color: grey; 48 | } 49 | 50 | div#headerbar h1 { 51 | color: white; 52 | font-size: 24pt; 53 | text-shadow: 0px 0px 10px black; 54 | margin: 0px; 55 | padding: 10px; 56 | display: inline-block; 57 | text-align: left; 58 | } 59 | 60 | div#headerbar ul { 61 | list-style-type: none; 62 | position: absolute; 63 | display: none; 64 | top: 26pt; 65 | left: 26pt; 66 | background: white; 67 | box-shadow: 0px 0px 30px #002; 68 | border-radius: 10px; 69 | padding: 5px; 70 | border: 2px solid #00A; 71 | } 72 | 73 | div#headerbar ul li { 74 | padding: 5px; 75 | font-family: sans-serif; 76 | font-size: 12pt; 77 | color: black; 78 | } 79 | 80 | div#headerbar ul li + li { 81 | border-top: 1px solid grey; 82 | } 83 | 84 | div#headerbar ul a, div#headerbar ul a:hover { 85 | color: #00F; 86 | text-shadow: none; 87 | } 88 | 89 | div#headerbar ul a:hover { 90 | background: #DDD; 91 | } 92 | 93 | div#headerbar > a { 94 | font-size: 16pt; 95 | text-shadow: 0px 0px 5px black; 96 | color: white; 97 | margin: 0px; 98 | padding: 10px; 99 | display: inline-block; 100 | } 101 | 102 | 103 | div#headerbar > span { 104 | font-size: 16pt; 105 | text-shadow: 0px 0px 5px white; 106 | color: black; 107 | margin: 0px; 108 | padding: 10px; 109 | display: inline-block; 110 | } 111 | 112 | h2 { 113 | font-size: 150%; 114 | font-family: inherit; 115 | font-weight: bold; 116 | font-style: normal; 117 | color: #F80; 118 | margin-top: 10px; 119 | margin-bottom: 10px; 120 | } 121 | 122 | h3 { 123 | font-size: 125%; 124 | font-family: inherit; 125 | font-weight: bold; 126 | font-style: normal; 127 | color: silver; 128 | margin-top: 10px; 129 | margin-bottom: 10px; 130 | } 131 | 132 | p.text { 133 | max-width: 80ex; 134 | } 135 | 136 | table { 137 | border-collapse: collapse; 138 | width: 100%; 139 | } 140 | 141 | td, th { 142 | border: 1px solid black; 143 | padding: 5px; 144 | text-align: left; 145 | vertical-align: top; 146 | } 147 | 148 | th { 149 | background: silver; 150 | font-weight: bold; 151 | } 152 | 153 | pre { 154 | font-family: monospace; 155 | font-size: 10pt; 156 | white-space: pre-wrap; 157 | } 158 | 159 | td pre { 160 | padding: 0px; 161 | margin: 0px; 162 | } 163 | 164 | .status_normal { 165 | } 166 | 167 | .status_ok { 168 | background: green; 169 | color: white; 170 | } 171 | 172 | .status_warn { 173 | background: yellow; 174 | color: black; 175 | } 176 | 177 | .status_fail { 178 | background: red; 179 | color: white; 180 | } 181 | 182 | .status_unknown { 183 | font-style: italic; 184 | background: white; 185 | color: gray; 186 | } 187 | 188 | .status_trivial { 189 | color: gray; 190 | } 191 | 192 | .status_running { 193 | opacity: 0.5; 194 | } 195 | 196 | a { 197 | text-decoration: none; 198 | color: inherit; 199 | } 200 | 201 | p a:link, p a:visited { 202 | font-weight: bold; 203 | color: #00F; 204 | } 205 | 206 | td.linkcell a:link, td.linkcell a:visited { 207 | color: #00F; 208 | } 209 | 210 | h1 a:link, h1 a:visited { 211 | color: white; 212 | } 213 | 214 | div#headerbar a:active, div#headerbar a:hover { 215 | text-shadow: 0px 0px 5px #FF0; 216 | } 217 | 218 | td.statuscell { 219 | margin: 0px; 220 | padding: 0px; 221 | } 222 | 223 | td.statuscell a { 224 | display: block; 225 | padding: 5px; 226 | margin: 0px; 227 | } 228 | 229 | .hidden { 230 | display: none; 231 | } 232 | 233 | [class^=deleted] { 234 | opacity: 0.5; 235 | text-decoration: line-through; 236 | } 237 | 238 | h2 a:hover, h2 a:active { 239 | text-shadow: 0px 0px 5px #88F; 240 | } 241 | 242 | div > pre { 243 | padding: 10px; 244 | border: 1px solid #F80; 245 | border-radius: 5px; 246 | background: #FED; 247 | } 248 | 249 | .meta_info { 250 | color: grey; 251 | } 252 | 253 | ul#service_status { 254 | list-style-type: none; 255 | margin: 20px 0px 20px 0px; 256 | padding: 0px; 257 | } 258 | 259 | ul#service_status:before { 260 | content: 'Service status: '; 261 | } 262 | 263 | ul#service_status li { 264 | display: inline-block; 265 | color: inherit; 266 | background: inherit; 267 | margin: 0px; 268 | padding: 2px; 269 | } 270 | 271 | ul#service_status li.status_fail { 272 | border: 1px solid red; 273 | border-left: 20px solid red; 274 | text-decoration: line-through; 275 | } 276 | 277 | ul#service_status li.status_ok { 278 | border: 1px solid green; 279 | border-left: 20px solid green; 280 | } 281 | 282 | form ol li { 283 | list-style: none; 284 | margin-bottom: 10px; 285 | } 286 | 287 | label { 288 | width: 10em; 289 | display: inline-block; 290 | text-align: right; 291 | vertical-align: top; 292 | } 293 | 294 | a:not(.hidden) + a > .fa, a:not(.hidden) + a.hidden + a > .fa { 295 | padding-left: 2ex; 296 | } 297 | 298 | input[type="submit"], input[type="text"], input[type="password"], input[type="number"], input[type="date"], input[type="time"], input[type="email"], select, textarea, input[type="file"]::file-selector-button, input[type="button"] { 299 | padding: 0.5ex; 300 | border: 1px solid #888; 301 | border-radius: 0.5ex; 302 | } 303 | 304 | input[type="submit"], select, input[type="file"]::file-selector-button, input[type="button"] { 305 | background: #EEE; 306 | color: #000; 307 | } 308 | 309 | input[type="submit"]:hover, select:hover, input[type="file"]::file-selector-button:hover, input[type="button"]:hover { 310 | background: #CCC; 311 | } 312 | 313 | input[type="text"], input[type="password"], input[type="number"], input[type="date"], input[type="time"], input[type="email"] { 314 | background: #FFF; 315 | } 316 | -------------------------------------------------------------------------------- /res/crab.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var mainmenu = $('#mainmenu'); 3 | $('#menubutton').click(function (event) { 4 | mainmenu.toggle(); 5 | event.stopPropagation(); 6 | }); 7 | $(document).click(function (event) { 8 | if ($(event.target).parents('#mainmenu').length === 0) { 9 | mainmenu.hide(); 10 | } 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /res/crontabs.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('[id^="show_raw_"]').click(function (event) { 3 | var crontab = event.target.id.replace('show_raw_', ''); 4 | $('#raw_' + crontab).show(); 5 | $('#show_raw_' + crontab).hide(); 6 | $('#hide_raw_' + crontab).show(); 7 | event.preventDefault(); 8 | }); 9 | $('[id^="hide_raw_"]').click(function (event) { 10 | var crontab = event.target.id.replace('hide_raw_', ''); 11 | $('#raw_' + crontab).hide(); 12 | $('#show_raw_' + crontab).show(); 13 | $('#hide_raw_' + crontab).hide(); 14 | event.preventDefault(); 15 | }); 16 | $('[id^="show_deleted_"]').click(function (event) { 17 | var crontab = event.target.id.replace('show_deleted_', ''); 18 | $('.deleted_' + crontab).show(); 19 | $('#table_' + crontab).show(); 20 | $('#show_deleted_' + crontab).hide(); 21 | $('#hide_deleted_' + crontab).show(); 22 | event.preventDefault(); 23 | }); 24 | $('[id^="hide_deleted_"]').click(function (event) { 25 | var crontab = event.target.id.replace('hide_deleted_', ''); 26 | $('.deleted_' + crontab).hide(); 27 | $('#show_deleted_' + crontab).show(); 28 | $('#hide_deleted_' + crontab).hide(); 29 | event.preventDefault(); 30 | }); 31 | $('#show_all_raw').click(function (event) { 32 | $('[id^="raw_"]').show(); 33 | $('[id^="show_raw_"]').hide(); 34 | $('[id^="hide_raw_"]').show(); 35 | $('#show_all_raw').hide(); 36 | $('#hide_all_raw').show(); 37 | event.preventDefault(); 38 | }); 39 | $('#hide_all_raw').click(function (event) { 40 | $('[id^="raw_"]').hide(); 41 | $('[id^="show_raw_"]').show(); 42 | $('[id^="hide_raw_"]').hide(); 43 | $('#show_all_raw').show(); 44 | $('#hide_all_raw').hide(); 45 | event.preventDefault(); 46 | }); 47 | $('#show_all_table').click(function (event) { 48 | $('[id^="table_"]').show(); 49 | $('[class^="deleted_"]').hide(); 50 | $('[id^="show_deleted_"]').show(); 51 | $('[id^="hide_deleted_"]').hide(); 52 | $('#show_all_table').hide(); 53 | $('#hide_all_table').show(); 54 | event.preventDefault(); 55 | }); 56 | $('#hide_all_table').click(function (event) { 57 | $('[id^="table_"]').hide(); 58 | $('[id^="show_deleted_"]').show(); 59 | $('[id^="hide_deleted_"]').hide(); 60 | $('#show_all_table').show(); 61 | $('#hide_all_table').hide(); 62 | event.preventDefault(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /res/editnotify.js: -------------------------------------------------------------------------------- 1 | var newRowNumber = 1; 2 | 3 | function addRow() { 4 | var nid = 'new_' + (newRowNumber ++); 5 | $('table#notifylist').append(notifyrowtemplate.replace(new RegExp('XXX', 'g'), nid)); 6 | $('#delete_' + nid).click(function (event) { 7 | deleteRow(nid); 8 | event.preventDefault(); 9 | }); 10 | } 11 | 12 | function deleteRow(notifyid) { 13 | var response = confirm('Delete notification for ' + 14 | $('#address_' + notifyid).val() + '?') 15 | if (response === true) { 16 | $('#row_' + notifyid).remove(); 17 | } 18 | } 19 | 20 | $(document).ready(function () { 21 | $('#add_notification').click(function (event) { 22 | addRow(); 23 | event.preventDefault(); 24 | }); 25 | 26 | $('[id^="delete_"]').click(function (event) { 27 | var notifyid = event.target.id.replace('delete_', ''); 28 | deleteRow(notifyid); 29 | event.preventDefault(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /res/favicon-disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/res/favicon-disconnect.png -------------------------------------------------------------------------------- /res/favicon-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/res/favicon-error.png -------------------------------------------------------------------------------- /res/favicon-stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/res/favicon-stopped.png -------------------------------------------------------------------------------- /res/favicon-warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/res/favicon-warn.png -------------------------------------------------------------------------------- /res/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grahambell/crab/8e0cc29b480cc6abdd256c5e445dcf6988c7a8ef/res/favicon.png -------------------------------------------------------------------------------- /res/jobevents.js: -------------------------------------------------------------------------------- 1 | function refreshJobEventsSuccess(data, text, xhr) { 2 | $('#jobevents').html(data); 3 | } 4 | 5 | function refreshJobEventsError(xhr, text, error) { 6 | alert('Failed to retrieve events: ' + text); 7 | } 8 | 9 | function refreshJobEvents(enddate) { 10 | var params = $('#eventsform').serialize(); 11 | 12 | if (enddate !== null) { 13 | params = params + '&enddate=' + encodeURIComponent(enddate); 14 | } 15 | 16 | $.ajax('/job/'+ jobidnumber + '?barerows=1&' + params, { 17 | dateType: 'html', 18 | success: refreshJobEventsSuccess, 19 | error: refreshJobEventsError, 20 | timeout: 10000 21 | }); 22 | 23 | var stateObj = {'enddate': enddate}; 24 | history.replaceState(stateObj, '', '/job/' + jobidnumber + '?' + params); 25 | } 26 | 27 | $(document).ready(function () { 28 | $('#eventsform').change(function (event) { 29 | if (history.state && ('enddate' in history.state)) { 30 | refreshJobEvents(history.state.enddate); 31 | } 32 | else { 33 | refreshJobEvents(null); 34 | } 35 | }); 36 | 37 | $('#eventslast').click(function (event) { 38 | refreshJobEvents(null); 39 | event.preventDefault(); 40 | }); 41 | 42 | $('#eventsprev').click(function (event) { 43 | refreshJobEvents(lastDateTime); 44 | event.preventDefault(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /res/joblist.js: -------------------------------------------------------------------------------- 1 | var sortByStatus = false; 2 | 3 | function updateStatusBox(id, status_, running) { 4 | var box = $('#status_' + id); 5 | if (status_ === null) { 6 | box.text('Unknown'); 7 | box.removeClass().addClass('status_unknown'); 8 | } 9 | else { 10 | box.text(crabStatusName(status_)); 11 | if (crabStatusIsOK(status_)) { 12 | box.removeClass().addClass('status_ok'); 13 | } 14 | else if (crabStatusIsWarning(status_)) { 15 | if (! box.hasClass('status_warn')) { 16 | box.removeClass().addClass('status_warn'); 17 | 18 | if (sortByStatus) { 19 | var tab = $('#joblistbody'); 20 | tab.prepend($('#row_' + id).detach()); 21 | } 22 | } 23 | } 24 | else if (! box.hasClass('status_fail')) { 25 | box.removeClass().addClass('status_fail'); 26 | 27 | if (sortByStatus) { 28 | var tab = $('#joblistbody'); 29 | tab.prepend($('#row_' + id).detach()); 30 | } 31 | } 32 | } 33 | if (running) { 34 | box.addClass('status_running'); 35 | } else if (box.hasClass('status_running')) { 36 | box.removeClass('status_running'); 37 | } 38 | } 39 | 40 | function updateReliabilityBox(id, reliability) { 41 | var box = $('#reliability_' + id); 42 | if (reliability > 100) { 43 | reliability = 100; 44 | } 45 | if (reliability < 0) { 46 | reliability = 0; 47 | } 48 | box.attr('title', 'Success rate: ' + reliability + '%'); 49 | var stars = ''; 50 | while (reliability >= 20) { 51 | stars = stars.concat('★'); 52 | reliability -= 20; 53 | } 54 | if (reliability >= 10) { 55 | stars = stars.concat('☆'); 56 | } 57 | box.html(stars); 58 | box.removeClass().addClass('status_normal'); 59 | } 60 | 61 | function updateInfo(data) { 62 | var id = data['id']; 63 | $('#host_' + id).text(data['host']); 64 | $('#user_' + id).text(data['user']); 65 | $('#command_' + id).text(data['command']); 66 | if (data['crabid'] !== null) { 67 | $('#crabid_' + id).text(data['crabid']); 68 | $('#crabid_' + id).removeClass(); 69 | } 70 | } 71 | 72 | function setFavicon(url) { 73 | var favicon = $('link[rel=icon]'); 74 | favicon.replaceWith(favicon.clone().attr('href', url)); 75 | } 76 | 77 | function updateServiceStatus(servstatus) { 78 | var statustext = ''; 79 | for (var id in servstatus) { 80 | if (servstatus[id]) { 81 | statustext = statustext.concat('
  • '); 82 | } 83 | else { 84 | statustext = statustext.concat('
  • '); 85 | } 86 | statustext = statustext.concat(id + '
  • '); 87 | } 88 | $('#service_status').html(statustext); 89 | } 90 | 91 | function updateStatus(data) { 92 | var statusdata = data['status']; 93 | for (var id in statusdata) { 94 | var job = statusdata[id]; 95 | 96 | if ($('#row_'+id).length == 0) { 97 | $('table#joblist').append(joblistrowtemplate.replace(new RegExp('XXX', 'g'), id)); 98 | $.ajax('/query/jobinfo/' + id, { 99 | dataType: 'json', 100 | success: updateInfo 101 | }); 102 | } 103 | 104 | updateStatusBox(id, job['status'], job['running']); 105 | updateReliabilityBox(id, job['reliability']); 106 | 107 | if (! job['scheduled']) { 108 | $('#reliability_' + id).append(''); 109 | } 110 | } 111 | 112 | var current_time = new Date(); 113 | $('#last_refresh').text(current_time.toString()); 114 | 115 | updateServiceStatus(data['service']); 116 | 117 | var service_ok = true; 118 | for (var id in data['service']) { 119 | service_ok = service_ok && data['service'][id]; 120 | } 121 | 122 | if (! service_ok) { 123 | setFavicon('/res/favicon-stopped.png'); 124 | } 125 | else if (data['numerror'] > 0) { 126 | setFavicon('/res/favicon-error.png'); 127 | } 128 | else if (data['numwarning'] > 0) { 129 | setFavicon('/res/favicon-warn.png'); 130 | } 131 | else { 132 | setFavicon('/res/favicon.png'); 133 | } 134 | } 135 | 136 | function refreshStatusOnceSuccess(data, text, xhr) { 137 | updateStatus(data); 138 | $('#command_refresh').children('span').removeClass('fa-spin'); 139 | } 140 | 141 | function refreshStatusOnceError(xhr, text, error) { 142 | $('#last_refresh').text('Failed to fetch status from server.'); 143 | $('#command_refresh').children('span').removeClass('fa-spin'); 144 | } 145 | 146 | function refreshStatusOnce() { 147 | $.ajax('/query/jobstatus?startid=0&alarmid=0&finishid=0', { 148 | dataType: 'json', 149 | success: refreshStatusOnceSuccess, 150 | error: refreshStatusOnceError 151 | }); 152 | $('#command_refresh').children('span').addClass('fa-spin'); 153 | } 154 | 155 | function refreshStatusCometSuccess(data, text, xhr) { 156 | updateStatus(data); 157 | refreshStatusCometLoop(data['startid'], data['alarmid'], data['finishid']); 158 | } 159 | 160 | function refreshStatusCometResume() { 161 | refreshStatusCometLoop(0, 0, 0); 162 | $('table#joblist').fadeTo(500, 1.0); 163 | } 164 | 165 | function refreshStatusCometError(xhr, text, error) { 166 | setTimeout(refreshStatusCometResume, 600000); 167 | $('table#joblist').fadeTo(500, 0.3); 168 | setFavicon(disconnectFavicon); 169 | } 170 | 171 | function refreshStatusCometLoop(startid, alarmid, finishid) { 172 | $.ajax('/query/jobstatus?startid=' + startid + '&alarmid=' + alarmid + '&finishid=' + finishid, { 173 | dataType: 'json', 174 | success: refreshStatusCometSuccess, 175 | error: refreshStatusCometError, 176 | timeout: 160000 177 | }); 178 | } 179 | 180 | function dashboardSorter(keyfield) { 181 | var sortdirection = 1; 182 | 183 | return function (event) { 184 | var arr = new Array(); 185 | var tab = $('#joblistbody'); 186 | 187 | tab.children().each(function (index) { 188 | arr.push({row: this, text: $(this).find('[id^=' + keyfield + '_]').text()}); 189 | }); 190 | 191 | arr.sort(function (a, b) { 192 | return a.text == b.text ? 0 : sortdirection * (a.text < b.text ? -1 : 1); 193 | }); 194 | 195 | for (var i in arr) { 196 | tab.append($(arr[i].row).detach()); 197 | } 198 | 199 | $('#joblisthead span').removeClass(); 200 | $('#preheading' + keyfield).addClass('fa fa-sort-' + (sortdirection > 0 ? 'down' : 'up')); 201 | 202 | sortdirection = - sortdirection; 203 | sortByStatus = false; 204 | event.preventDefault(); 205 | }; 206 | } 207 | 208 | function dashboardSortStatus(event) { 209 | var tab = $('#joblistbody'); 210 | tab.append(tab.children().has('.status_ok').detach()); 211 | tab.prepend(tab.children().has('.status_warn').detach()); 212 | tab.prepend(tab.children().has('.status_fail').detach()); 213 | sortByStatus = true; 214 | $('#joblisthead span').removeClass(); 215 | $('#preheadingstatus').addClass('fa fa-sort'); 216 | event.preventDefault(); 217 | } 218 | 219 | $(document).ready(function () { 220 | refreshStatusCometLoop(0, 0, 0); 221 | 222 | $('#command_refresh').click(function (event) { 223 | refreshStatusOnce(); 224 | event.preventDefault(); 225 | }); 226 | 227 | // Need to load the disconnected icon now because if the 228 | // server vanishes we will not be able to load it when 229 | // needed. 230 | var image = new Image(); 231 | image.onload = function () { 232 | var canvas = document.createElement('canvas'); 233 | canvas.width = 16; 234 | canvas.height = 16; 235 | var context = canvas.getContext('2d'); 236 | context.drawImage(image, 0, 0); 237 | disconnectFavicon = canvas.toDataURL('image/png'); 238 | }; 239 | image.src = '/res/favicon-disconnect.png'; 240 | 241 | var runningRule = null; 242 | var runningRuleOn = false; 243 | 244 | // Find the status_running CSS rule. 245 | for (var i = 0; i < document.styleSheets.length; i ++) { 246 | var sheet = document.styleSheets[i]; 247 | for (var j = 0; j < sheet.cssRules.length; j++) { 248 | var rule = sheet.cssRules[j]; 249 | if (rule.selectorText === '.status_running') { 250 | runningRule = rule; 251 | } 252 | } 253 | } 254 | 255 | // If we found it, make it flash. 256 | if (runningRule !== null) { 257 | setInterval(function () { 258 | runningRuleOn = ! runningRuleOn; 259 | if (runningRuleOn) { 260 | runningRule.style.cssText = 'opacity: 0.25;'; 261 | } 262 | else { 263 | runningRule.style.cssText = 'opacity: 1.0;'; 264 | } 265 | }, 500); 266 | } 267 | 268 | $('#headingstatus').click(dashboardSortStatus); 269 | $('#headinghost').click(dashboardSorter('host')); 270 | $('#headinguser').click(dashboardSorter('user')); 271 | $('#headingcrabid').click(dashboardSorter('crabid')); 272 | $('#headingcommand').click(dashboardSorter('command')); 273 | $('#headingreliability').click(dashboardSorter('reliability')); 274 | }); 275 | -------------------------------------------------------------------------------- /scripts/crabd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2012-2013 Science and Technology Facilities Council. 4 | # Copyright (C) 2016-2022 East Asian Observatory. 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import atexit 20 | import cherrypy 21 | from cherrypy.process.plugins import Daemonizer 22 | import logging 23 | from optparse import OptionParser 24 | import os 25 | import sys 26 | 27 | from crab.notify import CrabNotify 28 | from crab.service.clean import CrabCleanService 29 | from crab.service.monitor import CrabMonitor 30 | from crab.service.notify import CrabNotifyService 31 | from crab.server import CrabServer 32 | from crab.server.config import read_crabd_config, \ 33 | construct_log_handler, construct_store 34 | from crab.util.bus import CrabPlugin, priority 35 | from crab.util.filter import CrabEventFilter 36 | from crab.util.pid import pidfile_write, pidfile_running, pidfile_delete 37 | from crab.web.web import CrabWeb 38 | 39 | 40 | class CrabFacilities: 41 | def __init__(self, bus, config, pidfile=None): 42 | self.bus = bus 43 | self.config = config 44 | self.pidfile = pidfile 45 | 46 | def subscribe(self): 47 | self.bus.subscribe('start', self.start) 48 | 49 | @priority(70) 50 | def start(self): 51 | if self.pidfile is not None: 52 | self.bus.log('Writing Crab PID file: {}'.format(self.pidfile)) 53 | pidfile_write(self.pidfile, os.getpid()) 54 | atexit.register(pidfile_delete, self.pidfile) 55 | 56 | self.bus.log('Starting Crab facilities') 57 | 58 | store = self.get_store() 59 | self.bus.publish('crab-store', store) 60 | 61 | # Pass whole configuration to CrabNotify to allow it to 62 | # construct notification method objects. 63 | notifier = self.get_notifier(store) 64 | self.bus.publish('crab-notify', notifier) 65 | 66 | def get_store(self): 67 | if 'outputstore' in self.config: 68 | outputstore = construct_store(self.config['outputstore']) 69 | else: 70 | outputstore = None 71 | 72 | return construct_store(self.config['store'], outputstore) 73 | 74 | def get_notifier(self, store): 75 | return CrabNotify(self.config, store) 76 | 77 | 78 | def main(): 79 | # Handle command line arguments. 80 | parser = OptionParser() 81 | parser.add_option( 82 | '--pidfile', 83 | type='string', dest='pidfile', 84 | help='use PIDFILE to avoid re-running crabd', metavar='PIDFILE') 85 | parser.add_option( 86 | '--accesslog', 87 | type='string', dest='accesslog', 88 | help='Log file for HTTP requests', metavar='FILE') 89 | parser.add_option( 90 | '--errorlog', 91 | type='string', dest='errorlog', 92 | help='Log file for general messages', metavar='FILE') 93 | parser.add_option( 94 | '--import', 95 | type='string', dest='import_', 96 | help='import jobs and settings from file', metavar='JSONFILE') 97 | parser.add_option( 98 | '--export', 99 | type='string', dest='export', 100 | help='export jobs and settings to file', metavar='JSONFILE') 101 | parser.add_option( 102 | '--daemon', 103 | action='store_true', dest='daemon', 104 | help='Run in daemon mode') 105 | parser.add_option( 106 | '--passive', 107 | action='store_true', dest='passive', 108 | help='Run passive server (passive monitor, no notifications)') 109 | 110 | (options, args) = parser.parse_args() 111 | if len(args) != 0: 112 | parser.error('no arguments required') 113 | 114 | # Read configuration file. 115 | config = read_crabd_config() 116 | 117 | # Check for a pidfile if requested. 118 | pidfile = None 119 | if options.pidfile: 120 | pidfile = options.pidfile 121 | if pidfile is not None: 122 | if pidfile_running(pidfile): 123 | return 124 | 125 | facilities = CrabFacilities( 126 | cherrypy.engine, config=config, pidfile=pidfile) 127 | 128 | # Perform import/export operations if requested. 129 | if options.import_ or options.export: 130 | from crab.server.io import import_config, export_config 131 | store = facilities.get_store() 132 | 133 | if options.import_ and options.export: 134 | parser.error('import and export operations both requested') 135 | 136 | if options.import_: 137 | if options.import_ == '-': 138 | import_config(store=store, file_=sys.stdin) 139 | else: 140 | with open(options.import_, 'r') as file_: 141 | import_config(store=store, file_=file_) 142 | 143 | elif options.export: 144 | if options.export == '-': 145 | export_config(store=store, file_=sys.stdout) 146 | else: 147 | with open(options.export, 'w') as file_: 148 | export_config(store=store, file_=file_) 149 | return 150 | 151 | # Set up logging based on which log files are requested. 152 | cherrypy.log.screen = False 153 | 154 | if options.accesslog: 155 | handler = construct_log_handler( 156 | options.accesslog, config['access_log']) 157 | else: 158 | # Replicate previous CherryPy "screen" behavior. 159 | handler = logging.StreamHandler(sys.stdout) 160 | 161 | cherrypy.log.access_log.propagate = False 162 | cherrypy.log.access_log.addHandler(handler) 163 | 164 | if options.errorlog: 165 | handler = construct_log_handler( 166 | options.errorlog, config['error_log']) 167 | else: 168 | # Replicate previous CherryPy "screen" behavior. 169 | handler = logging.StreamHandler(sys.stderr) 170 | 171 | # Set handler on root logger and let CherryPy error_log to propagate to it. 172 | # Note: this duplicates the time in log messages from CherryPy but without 173 | # it non-CherryPy log messages would not show the time. 174 | handler.setFormatter(logging.Formatter( 175 | '%(asctime)s %(message)s', datefmt='%Y-%m-%dT%H:%M:%S')) 176 | 177 | root_logger = logging.getLogger() 178 | root_logger.addHandler(handler) 179 | root_logger.setLevel(logging.DEBUG) 180 | 181 | # Set up CherryPy Daemonizer if requested. 182 | if options.daemon: 183 | Daemonizer(cherrypy.engine).subscribe() 184 | 185 | facilities.subscribe() 186 | 187 | # Set a default timezone: applies to times shown in 188 | # notifications and on the web interface. 189 | CrabEventFilter.set_default_timezone(config['notify']['timezone']) 190 | 191 | CrabPlugin( 192 | cherrypy.engine, 'Monitor', CrabMonitor, 193 | passive=options.passive).subscribe() 194 | 195 | if not options.passive: 196 | CrabPlugin( 197 | cherrypy.engine, 'Notification', CrabNotifyService, 198 | config=config['notify'], notify=None).subscribe() 199 | 200 | # Construct cleaning service if requested. 201 | if ('clean' in config) and not options.passive: 202 | CrabPlugin( 203 | cherrypy.engine, 'Clean', CrabCleanService, 204 | config=config['clean']).subscribe() 205 | 206 | cherrypy.config.update(config) 207 | 208 | web = CrabWeb( 209 | config['crab']['home'], {}) 210 | web.subscribe() 211 | cherrypy.tree.mount(web, '/', config) 212 | 213 | server = CrabServer(cherrypy.engine) 214 | server.subscribe() 215 | cherrypy.tree.mount(server, '/api/0', {}) 216 | 217 | cherrypy.engine.start() 218 | cherrypy.engine.block() 219 | 220 | if __name__ == "__main__": 221 | main() 222 | -------------------------------------------------------------------------------- /scripts/crabd-check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Automatic start script for crabd, designed to be run from cron. 4 | # For example, to check that the server is running every 10 minutes: 5 | # 6 | # PATH=/path/to/crab/scripts:/bin:/usr/bin 7 | # PYTHONPATH=/path/to/crab/lib 8 | # 5-55/10 * * * * crabd-check 9 | # 10 | # It keeps track of the server's PID and restarts it if that process 11 | # is not running. It also redirects the output from crabd into 12 | # a log file. 13 | 14 | # Select directory for PID file. 15 | # Try /var/run otherwise use /tmp. 16 | 17 | if [ -w /var/run ] 18 | then 19 | RUNDIR=/var/run 20 | else 21 | RUNDIR=/tmp 22 | fi 23 | 24 | # Select directory for log file. 25 | # Try /var/log/crab otherwise /var/tmp/crab or /tmp. 26 | 27 | if [ -w /var/log/crab ] 28 | then 29 | LOGDIR=/var/log/crab 30 | elif [[ -w /var/log && ! -e /var/log/crab ]] 31 | then 32 | LOGDIR=/var/log/crab 33 | mkdir $LOGDIR 34 | elif [ -w /var/tmp/crab ] 35 | then 36 | LOGDIR=/var/tmp/crab 37 | elif [[ -w /var/tmp && ! -e /var/tmp/crab ]] 38 | then 39 | LOGDIR=/var/tmp/crab 40 | mkdir $LOGDIR 41 | else 42 | LOGDIR=/tmp 43 | fi 44 | 45 | # Set file names. 46 | 47 | PIDFILE=$RUNDIR/crabd-$UID.pid 48 | LOGFILEACC=$LOGDIR/crabd-$UID-access.log 49 | LOGFILEERR=$LOGDIR/crabd-$UID-error.log 50 | 51 | # Launch crabd using its own PID file mechanism. 52 | 53 | crabd --pidfile $PIDFILE --accesslog $LOGFILEACC --errorlog $LOGFILEERR --daemon > /dev/null 2>&1 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | from distutils.core import setup 6 | 7 | sys.path.insert(0, 'lib') 8 | from crab.version import version 9 | 10 | with open('README.rst') as file: 11 | long_description = file.read() 12 | 13 | 14 | def find_packages(base, path=[]): 15 | """Search for Python pacakges in the directory 'base'. 16 | Package names are built from the path components given 17 | by 'path'.""" 18 | packages = [] 19 | 20 | for name in os.listdir(os.path.join(base, *path)): 21 | pathname = path + [name] 22 | pathname_ = os.path.join(base, *pathname) 23 | 24 | if os.path.islink(pathname_): 25 | pass 26 | elif name == '__init__.py': 27 | packages.append('.'.join(path)) 28 | elif os.path.isdir(pathname_): 29 | packages.extend(find_packages(base, pathname)) 30 | 31 | return packages 32 | 33 | 34 | def find_files(inst, base=None, path=[]): 35 | """Search for data files in directory 'base', starting at 'path'. 36 | Files are to be installed in directory 'inst'. The 'base' 37 | directory name is not included in the install path.""" 38 | files = [] 39 | subdirs = [] 40 | 41 | if base: 42 | fullpath = os.path.join(base, *path) 43 | else: 44 | fullpath = os.path.join(*path) 45 | 46 | for name in os.listdir(fullpath): 47 | pathname = path + [name] 48 | pathname_ = os.path.join(fullpath, name) 49 | 50 | if os.path.islink(pathname_): 51 | pass 52 | elif os.path.isdir(pathname_): 53 | subdirs.extend(find_files(inst, base, pathname)) 54 | else: 55 | files.append(pathname_) 56 | 57 | if files: 58 | subdirs.append((os.path.join(inst, *path), files)) 59 | 60 | return subdirs 61 | 62 | 63 | setup( 64 | name='crab', 65 | version=version, 66 | author='Graham Bell', 67 | author_email='g.bell@eaobservatory.org', 68 | url='http://github.com/grahambell/crab', 69 | description='Cron alert board', 70 | long_description=long_description, 71 | package_dir={'': 'lib'}, 72 | packages=find_packages('lib'), 73 | scripts=[os.path.join('scripts', script) for script in [ 74 | 'crab', 75 | 'crabd', 76 | 'crabd-check', 77 | 'crabsh', 78 | ]], 79 | data_files=( 80 | [(os.path.join('share', 'doc', 'crab'), 81 | [os.path.join('doc', doc) for doc in [ 82 | 'crab.ini', 83 | 'crabd.ini', 84 | 'schema.sql', 85 | ]])] + 86 | find_files(os.path.join('share', 'crab'), None, ['res']) + 87 | find_files(os.path.join('share', 'crab'), None, ['templ'])), 88 | requires=[ 89 | 'CherryPy (>= 3.2.2)', 90 | 'crontab (>= 0.15)', 91 | 'Mako (>= 0.7.2)', 92 | 'pytz', 93 | ], 94 | classifiers=[ 95 | 'Development Status :: 3 - Alpha', 96 | 'License :: OSI Approved :: GNU General Public License' 97 | ' v3 or later (GPLv3+)', 98 | 'Programming Language :: Python', 99 | 'Topic :: System :: Monitoring' 100 | ] 101 | ) 102 | -------------------------------------------------------------------------------- /templ/base.html: -------------------------------------------------------------------------------- 1 | 2 | <%! 3 | scripts = [] 4 | %> 5 | 6 | 7 | Crab 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | % if self.attr.scripts: 16 | % for script in self.attr.scripts: 17 | % if script.startswith('dyn:'): 18 | 19 | % else: 20 | 21 | % endif 22 | % endfor 23 | % endif 24 | 25 | 26 |
    27 | 28 | 32 |

    Crab

    33 | <%block name="links" /> 34 |
    35 | 36 |
    37 | 38 | ${self.body()} 39 | 40 |
    41 | 42 | 43 | -------------------------------------------------------------------------------- /templ/confirm.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab.util.web import abbr 3 | %> 4 | <%inherit file="base.html"/> 5 | 6 | <%block name="links"> 7 | ${info['host'] | h} 8 | ${info['user'] | h} 9 | % if info['crabid'] is not None: 10 | ${info['crabid'] | h} 11 | % else: 12 | ${info['command'] | abbr} 13 | % endif 14 | ${title | h} 15 | 16 | 17 | 18 |

    Confirm Action

    19 | 20 |
    21 |

    22 | ${description | h} 23 |

    24 | 25 |

    26 | 27 | 28 | % if data is not UNDEFINED: 29 | % for (key, value) in data.items(): 30 | 31 | % endfor 32 | % endif 33 |

    34 |
    35 | -------------------------------------------------------------------------------- /templ/crontabs.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab.util.string import alphanum 3 | from crab.util.web import abbr 4 | 5 | scripts = ['crontabs'] 6 | %> 7 | <%inherit file="base.html"/> 8 | 9 | <%block name="links"> 10 | % if user is not None: 11 | ${user | h} 12 | % elif host is not None: 13 | ${host | h} 14 | % endif 15 | 16 | 17 | % for (key, joblist) in sorted(jobs.items()): 18 |

    19 | % if user is None: 20 | 21 | % else: 22 | 23 | % endif 24 | ${key | h} 25 |

    26 | 27 | <% hasDeleted = False %> 28 | 29 | 30 | 31 | 32 | 33 | 34 | % for job in joblist: 35 | % if job['deleted'] is None: 36 | 37 | % else: 38 | 39 | <% hasDeleted = True %> 40 | % endif 41 | % if job.get('time') is not None: 42 | 43 | % else: 44 | 45 | % endif 46 | % if job.get('crabid') is not None: 47 | 48 | % else: 49 | 50 | % endif 51 | 52 | 53 | % endfor 54 |
    TimeJob IDCommand
    55 | 56 |

    57 | % if hasDeleted: 58 | 59 | Show deleted. 60 | % endif 61 | % if raw[key] is not None: 62 | 63 | Show raw crontab. 64 | % endif 65 |

    66 | 67 | % if raw[key] is not None: 68 | 73 | % endif 74 | 75 | % endfor 76 | 77 | % if len(jobs) > 1: 78 |

    79 | 80 | Hide all tables. 81 | 82 | 83 | Show all raw crontabs. 84 |

    85 | % endif 86 | -------------------------------------------------------------------------------- /templ/dynres/crabutil.js: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab import CrabStatus 3 | 4 | statuses = set.union(CrabStatus.VALUES, CrabStatus.INTERNAL_VALUES) 5 | %> 6 | function crabStatusName(status_) { 7 | switch (status_) { 8 | % for status in statuses: 9 | case ${status}: 10 | return '${CrabStatus.get_name(status)}'; 11 | % endfor 12 | default: 13 | return 'Status ' + status_; 14 | } 15 | } 16 | 17 | function crabStatusIsOK(status_) { 18 | switch (status_) { 19 | % for status in statuses: 20 | % if CrabStatus.is_ok(status): 21 | case ${status}: 22 | % endif 23 | % endfor 24 | return true; 25 | default: 26 | return false; 27 | } 28 | } 29 | 30 | function crabStatusIsWarning(status_) { 31 | switch (status_) { 32 | % for status in statuses: 33 | % if CrabStatus.is_warning(status): 34 | case ${status}: 35 | % endif 36 | % endfor 37 | return true; 38 | default: 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /templ/editnotify.html: -------------------------------------------------------------------------------- 1 | <%! 2 | import pytz 3 | 4 | from crab.util.string import mergelines 5 | from crab.util.web import abbr 6 | 7 | scripts = ["editnotify"] 8 | %> 9 | <%inherit file="base.html"/> 10 | 11 | <%block name="links"> 12 | % if not match_mode: 13 | ${info['host'] | h} 14 | ${info['user'] | h} 15 | % if info['crabid'] is not None: 16 | ${info['crabid'] | h} 17 | % else: 18 | ${info['command'] | abbr} 19 | % endif 20 | % endif 21 | notifications 22 | 23 | 24 | <%def name="notifyrow(n)" filter="mergelines"> 25 | <% 26 | nid = n['notifyid'] 27 | %> 28 | 29 | % if match_mode: 30 | 37 | 44 | % endif 45 | 54 | 61 | 68 | 82 | 87 | 92 | 97 | 102 | 103 | Delete 104 | 105 | 106 | 107 | 108 | % if match_mode: 109 |

    Configure Host/User Notifications

    110 |

    111 | This page allows notifications to be configured 112 | by host name and/or by user name. 113 | If the host or user box is left blank, 114 | then the notifications 115 | will apply to all entries for that parameter. 116 |

    117 |

    118 | Notifications also can be attached to specific jobs 119 | by navigating to each job's information page. 120 |

    121 | % else: 122 |

    Configure Job Notifications

    123 |

    124 | This page allows notifications to be attached to 125 | this specific job. 126 |

    127 |

    128 | Notifications can also be configured 129 | by host name and/or by user name on the 130 | host/user notifications page. 131 |

    132 | % endif 133 | 134 | % if match_mode: 135 |
    136 | % else: 137 | 138 | % endif 139 | 140 | 141 | % if match_mode: 142 | 143 | 144 | % endif 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | % if notifications is not None: 156 | % for n in notifications: 157 | ${notifyrow(n)} 158 | % endfor 159 | % endif 160 |
    Host nameUser nameMethodAddressScheduleTimezoneSuccessesWarningsErrorsOutputActions
    161 |

    162 | Add notification. 163 |

    164 |

    165 | 166 |

    167 |
    168 | 169 | 173 | -------------------------------------------------------------------------------- /templ/job.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab import CrabStatus 3 | from crab.util.web import abbr 4 | 5 | scripts = ["jobevents"] 6 | %> 7 | <%inherit file="base.html"/> 8 | 9 | <%block name="links"> 10 | ${info['host'] | h} 11 | ${info['user'] | h} 12 | % if info['crabid'] is not None: 13 | ${info['crabid'] | h} 14 | % else: 15 | ${info['command'] | abbr} 16 | % endif 17 | 18 | 19 | 20 |

    Job Information

    21 | 22 | 23 | 33 | % if info["crabid"] != None: 34 | 35 | 36 | 37 | 38 | % endif 39 | 40 | 41 | 42 | 43 | 44 | 45 | % if info["time"] != None: 46 | 47 | % else: 48 | 49 | % endif 50 | 51 | % if info["timezone"] != None: 52 | 53 | 54 | 55 | 56 | % endif 57 | % if info["installed"] != None: 58 | 59 | 60 | 61 | 62 | % endif 63 | % if info["deleted"] != None: 64 | 65 | 66 | 67 | 68 | % endif 69 | % if config is not None: 70 | % if config['graceperiod'] is not None: 71 | 72 | 73 | 74 | 75 | % endif 76 | % if config['timeout'] is not None: 77 | 78 | 79 | 80 | 81 | % endif 82 | % endif 83 | % if notification is not None and len(notification): 84 | 85 | 86 | 93 | 94 | % endif 95 | % if config is not None: 96 | % if config['success_pattern'] is not None: 97 | 98 | 99 | 100 | 101 | % endif 102 | % if config['warning_pattern'] is not None: 103 | 104 | 105 | 106 | 107 | % endif 108 | % if config['fail_pattern'] is not None: 109 | 110 | 111 | 112 | 113 | % endif 114 | % endif 115 |
    Job ID${info["crabid"] | h}
    Command${info["command"] | h}
    Schedule${info["time"] | h}Unknown
    Time zone${info["timezone"] | h}
    Installed${info["installed"] | h}
    Deleted${info["deleted"] | h}
    Grace period${config["graceperiod"] | h} minutes
    Time-out${config["timeout"] | h} minutes
    Notifications${len(notification)} explicit 87 | % if len(notification) == 1: 88 | notification 89 | % else: 90 | notifications 91 | % endif 92 |
    Success pattern${config["success_pattern"] | h}
    Warning pattern${config["warning_pattern"] | h}
    Failure pattern${config["fail_pattern"] | h}
    116 | 117 |

    118 | Edit configuration. 119 | Edit notifications. 120 |

    121 | % if status['status'] is not None and not CrabStatus.is_ok(status['status']): 122 |

    123 | Clear status. (${CrabStatus.get_name(status['status']) | h}) 124 |

    125 | % endif 126 | % if config is not None and config['inhibit']: 127 |

    128 | Resume inhibited job. 129 |

    130 | % endif 131 | 132 | % if config is not None: 133 | % if config["note"] is not None: 134 |

    Notes

    135 | % for noteline in config["note"].splitlines(): 136 | ${noteline | h}
    137 | % endfor 138 | % endif 139 | % endif 140 | 141 | 142 |

    Job History

    143 | 144 |
    145 |

    146 | 155 |     156 | 157 | Show all 158 |     159 | Previous 160 | Last 161 |

    162 |
    163 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | <%include file="jobevents.html" /> 180 | 181 |
    StatusDate and TimeCommandDuration
    182 | -------------------------------------------------------------------------------- /templ/jobconfig.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab.util.web import abbr 3 | %> 4 | <%inherit file="base.html"/> 5 | 6 | <%block name="links"> 7 | ${info['host'] | h} 8 | ${info['user'] | h} 9 | % if info['crabid'] is not None: 10 | ${info['crabid'] | h} 11 | % else: 12 | ${info['command'] | abbr} 13 | % endif 14 | configuration 15 | 16 | 17 |

    Job Configuration

    18 | 19 | % if config is None: 20 |

    Add New Job Configuration

    21 | % else: 22 |

    Edit Job Configuration

    23 | % endif 24 | 25 |
    26 |

    27 | Any parameter which is left blank here will use the default value. 28 |

    29 |
      30 |
    1. 31 | 32 | 37 | minutes 38 |
    2. 39 |
    3. 40 | 41 | 46 | minutes 47 |
    4. 48 |
    49 |

    50 | These patterns (if set) will be matched against the job's output. 51 |

    52 |
      53 |
    1. 54 | 55 | 60 |
    2. 61 |
    3. 62 | 63 | 68 |
    4. 69 |
    5. 70 | 71 | 76 |
    6. 77 |
    78 |

    79 | Here you can enter any notes about this job. 80 |

    81 |
      82 |
    1. 83 | 84 | % if config is not None and config['note'] is not None: 85 | 86 | % else: 87 | 88 | % endif 89 |
    2. 90 |
    91 |

    92 | This setting requests that the job not be run. 93 |

    94 |
      95 |
    1. 96 | 97 | % if config is not None and config['inhibit']: 98 | 99 | % else: 100 | 101 | % endif 102 |
    2. 103 |
    3. 104 | 105 | 106 |
    4. 107 |
    108 |
    109 | 110 | % if config is None: 111 | % if orphan: 112 |

    Re-link Orphaned Job Configuration

    113 |

    114 | There are orphaned configurations, 115 | relating to deleted jobs. 116 |

    117 |

    118 | If this job is the continuation of one which 119 | has been deleted, its configuration can be 120 | transferred by selecting it for re-linking. 121 |

    122 |
    123 |

    124 | 135 | 136 |

    137 |
    138 | % endif 139 | % endif 140 | 141 |

    Job Control

    142 | 143 |

    144 | 145 | % if info['deleted'] is None: 146 | Delete 147 | % else: 148 | Undelete 149 | % endif 150 | job. 151 | 152 |

    153 | 154 |

    Change Job ID

    155 | 156 |
    157 |
      158 |
    1. 159 | 160 | 167 |
    2. 168 |
    3. 169 | 170 | 171 |
    4. 172 |
    173 |
    174 | -------------------------------------------------------------------------------- /templ/jobevents.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab import CrabEvent, CrabStatus 3 | %> 4 | % for event in events: 5 | % if event["type"] == 1: 6 | 7 | % elif event["type"] == 2: 8 | 9 | % elif event["type"] == 3: 10 | 11 | % else: 12 | 13 | % endif 14 | % if event["status"] is None: 15 | ${CrabEvent.get_name(event['type']) | h} 16 | % else: 17 | <% 18 | if CrabStatus.is_trivial(event['status']): 19 | cell_class = 'status_trivial' 20 | elif CrabStatus.is_ok(event['status']): 21 | cell_class = 'status_ok' 22 | elif CrabStatus.is_warning(event['status']): 23 | cell_class = 'status_warn' 24 | elif CrabStatus.is_error(event['status']): 25 | cell_class = 'status_fail' 26 | else: 27 | cell_class = 'status_warn' 28 | 29 | if event['type'] == CrabEvent.FINISH: 30 | cell_link = '/job/' + str(id) + '/output/' + str(event['eventid']) 31 | else: 32 | cell_link = None 33 | 34 | cell_text = CrabStatus.get_name(event['status']) 35 | %> 36 | % if cell_link is None: 37 | ${cell_text | h} 38 | % else: 39 | 40 | ${cell_text | h} 41 | 42 | % endif 43 | % endif 44 | ${event["datetime"] | h} 45 | % if event["command"] is not None: 46 | ${event["command"] | h} 47 | % else: 48 |   49 | % endif 50 | % if event.get('duration') is None: 51 |   52 | % else: 53 | ${event['duration'] | h} 54 | % endif 55 | 56 | % endfor 57 | 60 | 61 | -------------------------------------------------------------------------------- /templ/joblist.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab.util.string import mergelines 3 | from crab.util.web import abbr 4 | 5 | scripts = ["dyn:crabutil", "joblist"] 6 | %> 7 | <%inherit file="base.html"/> 8 | 9 | <%def name="joblistrow(job)" filter="mergelines"> 10 | 11 | Loading 12 | % if job.get("host") is not None: 13 | ${job["host"] | h} 14 | % else: 15 |   16 | % endif 17 | % if job.get('user') is not None: 18 | ${job['user'] | h} 19 | % else: 20 |   21 | % endif 22 | % if job.get('crabid') is not None: 23 | ${job["crabid"] | h} 24 | % else: 25 | Unspecified 26 | % endif 27 | 28 | % if job.get('command') is not None: 29 | ${job["command"] | abbr} 30 | % else: 31 |   32 | % endif 33 | 34 | Loading 35 | 36 | 37 | 38 |

    Dashboard

    39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | % for job in jobs: 53 | ${joblistrow(job)} 54 | % endfor 55 | 56 |
    StatusHostUserJob IDCommandReliability
    57 | 58 | 62 | 63 |
      64 | 65 |

      66 | Refresh 67 | 68 | Last refreshed Never 69 | 70 |

      71 | -------------------------------------------------------------------------------- /templ/joboutput.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from crab import CrabStatus 3 | from crab.util.web import abbr 4 | 5 | scripts = ['ansi_up', 'coloroutput'] 6 | %> 7 | <%inherit file="base.html"/> 8 | 9 | <%block name="links"> 10 | ${info['host'] | h} 11 | ${info['user'] | h} 12 | % if info['crabid'] is not None: 13 | ${info['crabid'] | h} 14 | % else: 15 | ${info['command'] | abbr} 16 | % endif 17 | output 18 | 19 | 20 |

      Job Output

      21 | 22 | % if next or prev: 23 |

      24 | % if prev: 25 | Previous 26 | % endif 27 | % if next: 28 | Next 29 | Last 30 | % endif 31 |

      32 | % endif 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
      Date and Time${finish['datetime'] | h}
      Command${finish['command'] | h}
      Status${CrabStatus.get_name(finish['status']) | h}
      Standard output
      ${stdout.strip() | h}
      Standard error
      ${stderr.strip() | h}
      56 | -------------------------------------------------------------------------------- /templ/report/basic.html: -------------------------------------------------------------------------------- 1 | <%! 2 | 3 | from crab import CrabEvent, CrabStatus 4 | 5 | %> 6 | 7 | Crab Report 8 | 9 | 10 | ${summary('error', 'Jobs with Errors')} 11 | ${summary('warning', 'Jobs with Warnings')} 12 | ${summary('ok', 'Successful Jobs')} 13 | 14 | <%def name="summary(section, title)"> 15 | <% 16 | jobs = getattr(report, section) 17 | %> 18 | % if jobs: 19 |

      ${title | h}

      20 | 21 | 22 | % for id_ in jobs: 23 | <% 24 | info = report.info[id_] 25 | %> 26 | 27 | 28 | 29 | 30 | 31 | % endfor 32 |
      ${info['host'] | h}${info['user'] | h}${info['title'] | h}
      33 | % endif 34 | 35 | 36 |

      Event Listing

      37 | 38 | % for id_ in set.union(report.error, report.warning, report.ok): 39 | <% 40 | info = report.info[id_] 41 | %> 42 |

      43 | 44 | ${info['user'] | h} @ 45 | ${info['host'] | h } : 46 | ${info['title'] | h} 47 |

      48 | 49 | 50 | % for event in report.events[id_]: 51 | 52 | 53 | % if event['type'] == CrabEvent.FINISH: 54 | 57 | % else: 58 | 59 | % endif 60 | 61 | % if event['type'] == CrabEvent.FINISH and report.stdout: 62 | 73 | % endif 74 | 75 | % endfor 76 |
      ${CrabEvent.get_name(event['type']) | h} 55 | ${CrabStatus.get_name(event['status']) | h} 56 | ${CrabStatus.get_name(event['status']) | h}${event['datetime'] | h} 63 | % if event['eventid'] in report.stdout and report.stdout[event['eventid']]: 64 |
      ${report.stdout[event['eventid']].strip() | h}
      65 | % endif 66 | % if event['eventid'] in report.stderr and report.stderr[event['eventid']]: 67 | % if event['eventid'] in report.stdout and report.stdout[event['eventid']]: 68 |

      Standard error:

      69 | % endif 70 |
      ${report.stderr[event['eventid']].strip() | h}
      71 | % endif 72 |
      77 | % endfor 78 | 79 | 80 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from crab.store.sqlite import CrabStoreSQLite 4 | 5 | 6 | class CrabDBTestCase(TestCase): 7 | def setUp(self): 8 | with open('doc/schema.sql') as file: 9 | schema = file.read() 10 | 11 | self.store = CrabStoreSQLite(':memory:') 12 | self.store.lock.conn.executescript(schema) 13 | 14 | def tearDown(self): 15 | self.store.lock.conn.close() 16 | -------------------------------------------------------------------------------- /test/test_crontab.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from crab.util.crontab import parse_crontab, write_crontab 4 | 5 | 6 | class CrontabTestCase(TestCase): 7 | def test_read_write(self): 8 | """Test crontab read and write functions.""" 9 | 10 | crontab_orig = [ 11 | 'CRON_TZ=Europe/Berlin', 12 | '* * * * * CRABID=job_one command_one', 13 | '0 15 * * * command_two', 14 | '0 0 1 4 * date +\%Y\%m\%d', 15 | '59 23 12 31 * echo%a\%b%c\%d', 16 | '1 2 3 4 * CRABID=cal CRABCLIENTHOSTNAME=b CRABUSERNAME=a cal', 17 | ] 18 | 19 | (jobs, warnings) = parse_crontab(crontab_orig) 20 | 21 | self.assertEqual(jobs, [ 22 | {'crabid': 'job_one', 'command': 'command_one', 23 | 'time': '* * * * *', 'timezone': 'Europe/Berlin', 24 | 'rule': '* * * * * CRABID=job_one command_one', 25 | 'input': None, 'vars': {}}, 26 | {'crabid': None, 'command': 'command_two', 27 | 'time': '0 15 * * *', 'timezone': 'Europe/Berlin', 28 | 'rule': '0 15 * * * command_two', 29 | 'input': None, 'vars': {}}, 30 | {'crabid': None, 'command': 'date +%Y%m%d', 31 | 'time': '0 0 1 4 *', 'timezone': 'Europe/Berlin', 32 | 'rule': '0 0 1 4 * date +\%Y\%m\%d', 33 | 'input': None, 'vars': {}}, 34 | {'crabid': None, 'command': 'echo', 35 | 'time': '59 23 12 31 *', 'timezone': 'Europe/Berlin', 36 | 'rule': '59 23 12 31 * echo%a\%b%c\%d', 37 | 'input': 'a%b\nc%d', 'vars': {}}, 38 | {'crabid': 'cal', 'command': 'cal', 39 | 'time': '1 2 3 4 *', 'timezone': 'Europe/Berlin', 40 | 'rule': '1 2 3 4 * CRABID=cal CRABCLIENTHOSTNAME=b CRABUSERNAME=a cal', 41 | 'input': None, 42 | 'vars': {'CRABCLIENTHOSTNAME': 'b', 'CRABUSERNAME': 'a'}}, 43 | ]) 44 | 45 | self.assertEqual(warnings, []) 46 | 47 | crontab_written = write_crontab(jobs) 48 | 49 | self.assertEqual(crontab_written, crontab_orig) 50 | -------------------------------------------------------------------------------- /test/test_io.py: -------------------------------------------------------------------------------- 1 | from unittest import main, TestCase 2 | 3 | from crab.server.io import _filter_dict, _notify_key 4 | 5 | 6 | class ServerIOTestCase(TestCase): 7 | def test_filter_dict(self): 8 | d = {'a': 100, 'b': 200, 'c': 0, 'd': 1} 9 | keys = ['a', '*c', '*d'] 10 | 11 | result = _filter_dict(None, keys) 12 | self.assertIsNone(result) 13 | 14 | result = _filter_dict(d, keys) 15 | self.assertIsInstance(result, dict) 16 | self.assertEqual(set(result.keys()), set(('a', 'c', 'd'))) 17 | self.assertEqual(result['a'], 100) 18 | self.assertIs(result['c'], False) 19 | self.assertIs(result['d'], True) 20 | 21 | def test_notify_key(self): 22 | notification = { 23 | 'host': 'localhost', 24 | 'user': 'user', 25 | 'method': 'email', 26 | 'address': 'user@localhost', 27 | 'time': '* * * * *', 28 | 'timezone': 'Pacific/Honolulu', 29 | } 30 | 31 | result = _notify_key(notification) 32 | self.assertIsInstance(result, tuple) 33 | self.assertEqual(result, ('email', 'user@localhost', 34 | '* * * * *', 'Pacific/Honolulu')) 35 | 36 | result = _notify_key(notification, match=True) 37 | self.assertIsInstance(result, tuple) 38 | self.assertEqual(result, ('localhost', 'user', 39 | 'email', 'user@localhost', 40 | '* * * * *', 'Pacific/Honolulu')) 41 | -------------------------------------------------------------------------------- /test/test_store.py: -------------------------------------------------------------------------------- 1 | from . import CrabDBTestCase 2 | 3 | 4 | class JobIdentifyTestCase(CrabDBTestCase): 5 | def test_identify(self): 6 | """Test that _check_job correctly identifies jobs.""" 7 | 8 | id_ = self.store.check_job('host1', 'user1', None, 'command1') 9 | self.assertEqual(id_, 1, 'First job should have ID 1') 10 | 11 | id_ = self.store.check_job('host2', 'user1', None, 'command1') 12 | self.assertEqual(id_, 2, 'Job on new host should have new ID') 13 | 14 | id_ = self.store.check_job('host1', 'user2', None, 'command1') 15 | self.assertEqual(id_, 3, 'Job for new user should have new ID') 16 | 17 | id_ = self.store.check_job('host1', 'user1', None, 'command2') 18 | self.assertEqual(id_, 4, 'Job for new command should have new ID') 19 | 20 | id_ = self.store.check_job('host1', 'user1', None, 'command1') 21 | self.assertEqual(id_, 1, 'Original job should have ID 1') 22 | 23 | id_ = self.store.check_job('host1', 'user1', 'crabid1', 'command1') 24 | self.assertEqual(id_, 1, 'Original job should have ID 1 with crabid') 25 | 26 | id_ = self.store.check_job('host1', 'user1', 'crabid1', 'command3') 27 | self.assertEqual(id_, 1, 'Original job should have ID 1 by crabid') 28 | 29 | id_ = self.store.check_job('host1', 'user1', None, 'command3') 30 | self.assertEqual(id_, 1, 'Original job should have ID 1 by new cmd') 31 | 32 | id_ = self.store.check_job('host1', 'user1', None, 'command1') 33 | self.assertEqual(id_, 5, 'Original command should now be a new job') 34 | 35 | id_ = self.store.check_job('host1', 'user1', 'crabid2', 'command4') 36 | self.assertEqual(id_, 6, 'Additional new command creates new job') 37 | 38 | id_ = self.store.check_job('host1', 'user1', 'crabid3', 'command4') 39 | self.assertEqual(id_, 7, 'New ID should create another new job') 40 | -------------------------------------------------------------------------------- /test/test_storethread.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from random import sample 4 | from threading import Thread 5 | import sys 6 | 7 | from . import CrabDBTestCase 8 | 9 | DUPLICATES = 200 10 | ITERATIONS = 10 11 | 12 | 13 | class StoreThreadsTestCase(CrabDBTestCase): 14 | def test_thread(self): 15 | """Test for threading problems. 16 | 17 | Spawn a lot of threads each performing actions on the database. 18 | The intention is to cover all public methods of the store 19 | object. If any of the methods are not thread-safe then 20 | this kind of test ought to stand a chance of detecting 21 | the issue.""" 22 | 23 | threads = [] 24 | 25 | for i in range(DUPLICATES): 26 | threads.append(CronTabTester(self.store)) 27 | threads.append(CronJobTester(self.store)) 28 | threads.append(CronLogTester(self.store)) 29 | 30 | for t in threads: 31 | t.start() 32 | 33 | for t in threads: 34 | t.join() 35 | 36 | for t in threads: 37 | self.assertEqual(t.exceptions, 0) 38 | 39 | 40 | class RandomTester(Thread): 41 | def __init__(self, store): 42 | Thread.__init__(self) 43 | self.exceptions = 0 44 | self.store = store 45 | self.user = sample(['usera', 'userb', 'userc', 'userd', 46 | 'usere', 'userf', 'userg', 'userh'], 1)[0] 47 | self.host = sample(['hosta', 'hostb', 'hostc', 'hostd', 48 | 'hoste', 'hostf', 'hostg', 'hosth'], 1)[0] 49 | 50 | def run(self): 51 | for i in range(ITERATIONS): 52 | try: 53 | self.run_iteration() 54 | except: 55 | self.exceptions += 1 56 | raise 57 | print('.', end='') 58 | sys.stdout.flush() 59 | 60 | 61 | class CronTabTester(RandomTester): 62 | def run_iteration(self): 63 | self.store.save_crontab(self.host, self.user, self.randomtab()) 64 | self.store.get_crontab(self.host, self.user) 65 | self.store.get_raw_crontab(self.host, self.user) 66 | 67 | def randomtab(self): 68 | return list(sample(['CRON_TZ=Europe/Paris', 69 | 'CRABSHELL=/bin/tcsh', 70 | '#comment', 71 | '* * * * * cal', 72 | '0 * * * * date', 73 | '0 0 * * * uname', 74 | '0 0 * * 1-5 gnubeep', 75 | '* * * * * CRABID=minutely /bin/minutely.sh'], 3)) 76 | 77 | 78 | class CronJobTester(RandomTester): 79 | def run_iteration(self): 80 | self.store.get_jobs() 81 | self.store.get_jobs(self.host, self.user) 82 | self.store.get_jobs(self.host, self.user, include_deleted=True) 83 | self.store.check_job(self.host, self.user, None, sample([ 84 | 'command1', 'command2', 'command3'], 1)[0]) 85 | self.store.get_notifications() 86 | 87 | 88 | class CronLogTester(RandomTester): 89 | def __init__(self, store): 90 | RandomTester.__init__(self, store) 91 | self.s = 0 92 | self.w = 0 93 | self.f = 0 94 | 95 | def run_iteration(self): 96 | c = ['command1', 'command2', 'command3', 'command4'] 97 | self.store.log_start(self.host, self.user, None, sample(c, 1)[0]) 98 | self.store.log_finish(self.host, self.user, None, sample(c, 1)[0], 99 | 1, 'stdout', 'stderr') 100 | id_ = self.store.check_job(self.host, self.user, None, sample(c, 1)[0]) 101 | self.store.log_alarm(id_, -1) 102 | self.store.get_job_info(id_) 103 | # Need to add the write_config method when implemented 104 | self.store.get_job_config(id_) 105 | self.store.get_job_finishes(id_, 10) 106 | self.store.get_job_events(id_, 10) 107 | 108 | es = self.store.get_events_since(self.s, self.w, self.f) 109 | for e in es: 110 | eid = e['eventid'] 111 | if e['type'] == 1 and eid > self.s: 112 | self.s = eid 113 | elif e['type'] == 2 and eid > self.w: 114 | self.w = eid 115 | elif e['type'] == 3 and eid > self.f: 116 | self.f = eid 117 | 118 | self.store.get_fail_events(10) 119 | self.store.get_job_output(0, self.host, self.user, id_, None) 120 | -------------------------------------------------------------------------------- /test/test_type.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from crab import CrabStatus, CrabEvent 4 | 5 | 6 | class CrabTypeTestCase(TestCase): 7 | def test_status(self): 8 | self.assertEqual(CrabStatus.get_name(1), 'Failed') 9 | self.assertEqual(CrabStatus.get_name(-1), 'Late') 10 | self.assertEqual(CrabStatus.get_name(42), 'Status 42') 11 | self.assertEqual(CrabStatus.get_name(None), 'Undefined') 12 | 13 | self.assertTrue(CrabStatus.is_ok(CrabStatus.SUCCESS)) 14 | self.assertTrue(CrabStatus.is_ok(CrabStatus.LATE)) 15 | self.assertFalse(CrabStatus.is_ok(CrabStatus.WARNING)) 16 | self.assertFalse(CrabStatus.is_ok(CrabStatus.FAIL)) 17 | 18 | self.assertFalse(CrabStatus.is_error(CrabStatus.SUCCESS)) 19 | self.assertFalse(CrabStatus.is_error(CrabStatus.LATE)) 20 | self.assertFalse(CrabStatus.is_error(CrabStatus.WARNING)) 21 | self.assertTrue(CrabStatus.is_error(CrabStatus.FAIL)) 22 | 23 | def test_event(self): 24 | self.assertEqual(CrabEvent.get_name(1), 'Started') 25 | self.assertEqual(CrabEvent.get_name(42), 'Event 42') 26 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import doctest 3 | import crab.util.string 4 | 5 | 6 | def load_tests(loader, tests, ignore): 7 | tests.addTests(doctest.DocTestSuite(crab.util.string)) 8 | return tests 9 | -------------------------------------------------------------------------------- /util/fromoutputstore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2012 Science and Technology Facilities Council. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from crab.server.config import read_crabd_config, construct_store 19 | 20 | from tooutputstore import copy_data 21 | 22 | 23 | def main(): 24 | config = read_crabd_config() 25 | 26 | store = construct_store(config['store']) 27 | outputstore = construct_store(config['outputstore']) 28 | 29 | copy_data(store, outputstore, store) 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /util/tooutputstore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2012 Science and Technology Facilities Council. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from __future__ import print_function 19 | 20 | from crab.server.config import read_crabd_config, construct_store 21 | 22 | 23 | def main(): 24 | config = read_crabd_config() 25 | 26 | store = construct_store(config['store']) 27 | outputstore = construct_store(config['outputstore']) 28 | 29 | copy_data(store, store, outputstore) 30 | 31 | 32 | def copy_data(indexstore, instore, outstore): 33 | """Copies data from the instore to the outstore, using the 34 | indexstore as the one from which to get the list of 35 | events and crontabs to copy.""" 36 | 37 | hostuser = set() 38 | 39 | for job in indexstore.get_jobs(include_deleted=True): 40 | hostuser.add((job['host'], job['user'])) 41 | 42 | print('Processing job:', job['id']) 43 | 44 | for finish in indexstore.get_job_finishes(job['id'], limit=None): 45 | (stdout, stderr) = instore.get_job_output( 46 | finish['finishid'], job['host'], job['user'], 47 | job['id'], job['crabid']) 48 | 49 | if stdout or stderr: 50 | 51 | outstore.write_job_output( 52 | finish['finishid'], job['host'], job['user'], 53 | job['id'], job['crabid'], 54 | stdout, stderr) 55 | 56 | for (host, user) in hostuser: 57 | print('Processing crontab:', user, '@', host) 58 | 59 | crontab = instore.get_raw_crontab(host, user) 60 | 61 | if crontab is not None: 62 | outstore.write_raw_crontab(host, user, crontab) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /util/update_2012-10-15.sql: -------------------------------------------------------------------------------- 1 | -- This SQL script updates the SQLite database 2 | -- to the new schema, on the assumption that 3 | -- no job records have been deleted from the job 4 | -- table. (Crab would not have done so.) 5 | -- Backing up the database is recommended before 6 | -- running this script. 7 | 8 | BEGIN TRANSACTION; 9 | 10 | ALTER TABLE jobwarn RENAME TO jobalarm; 11 | 12 | ALTER TABLE job RENAME TO job_old; 13 | 14 | CREATE TABLE job ( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | host VARCHAR(255) NOT NULL, 17 | user VARCHAR(255) NOT NULL, 18 | command VARCHAR(255) NOT NULL, 19 | crabid VARCHAR(255), 20 | time VARCHAR(255), 21 | timezone VARCHAR(255), 22 | installed VARCHAR(255) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | deleted VARCHAR(255), 24 | UNIQUE (host, user, crabid) 25 | ); 26 | 27 | INSERT INTO job 28 | (host, user, command, crabid, time, timezone, installed, deleted) 29 | SELECT host, user, command, jobid, time, timezone, installed, deleted 30 | FROM job_old 31 | ORDER BY id ASC; 32 | 33 | DROP TABLE job_old; 34 | 35 | CREATE INDEX job_crabid ON job (crabid); 36 | CREATE INDEX job_host ON job (host); 37 | CREATE INDEX job_user ON job (user); 38 | CREATE INDEX job_command ON job (command); 39 | CREATE INDEX job_installed ON job (installed); 40 | CREATE INDEX job_deleted ON job (deleted); 41 | 42 | COMMIT; 43 | -------------------------------------------------------------------------------- /util/update_2014-07-09.sql: -------------------------------------------------------------------------------- 1 | -- This SQL script updates the SQLite database to add 2 | -- new columns to the job configuration table. 3 | -- Backing up the database is recommended before 4 | -- running this script. 5 | 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE jobconfig ADD COLUMN success_pattern VARCHAR(255) DEFAULT NULL; 9 | ALTER TABLE jobconfig ADD COLUMN warning_pattern VARCHAR(255) DEFAULT NULL; 10 | ALTER TABLE jobconfig ADD COLUMN fail_pattern VARCHAR(255) DEFAULT NULL; 11 | ALTER TABLE jobconfig ADD COLUMN note TEXT DEFAULT NULL; 12 | 13 | COMMIT; 14 | -------------------------------------------------------------------------------- /util/update_2014-08-05.sql: -------------------------------------------------------------------------------- 1 | -- This SQL script updates the SQLite database to add 2 | -- a new column to the job configuration table. 3 | -- Backing up the database is recommended before 4 | -- running this script. 5 | 6 | BEGIN TRANSACTION; 7 | 8 | ALTER TABLE jobconfig ADD COLUMN inhibit BOOLEAN NOT NULL DEFAULT 0; 9 | 10 | COMMIT; 11 | -------------------------------------------------------------------------------- /util/update_2016-01-06_mysql.sql: -------------------------------------------------------------------------------- 1 | -- This SQL script updates a MySQL database to alter the foreign 2 | -- key constraint on joboutput.finishid to allow deletions to 3 | -- cascade. You will need to apply this update if you wish to use 4 | -- the automated cleaning service with an existing MySQL-based 5 | -- installation which has been run without a separate output store. 6 | -- You may wish to check the name of the existing foreign key 7 | -- constraint using "SHOW CREATE TABLE joboutput" and update the name 8 | -- used in this script if necessary. 9 | -- 10 | -- Backing up the database is recommended before running this script. 11 | 12 | ALTER TABLE joboutput 13 | DROP FOREIGN KEY `joboutput_ibfk_1`; 14 | 15 | ALTER TABLE joboutput 16 | ADD FOREIGN KEY (finishid) REFERENCES jobfinish(id) 17 | ON DELETE CASCADE ON UPDATE RESTRICT; 18 | -------------------------------------------------------------------------------- /util/update_2016-01-06_sqlite.sql: -------------------------------------------------------------------------------- 1 | -- This SQL script updates a SQLite database to alter the foreign 2 | -- key constraint on joboutput.finishid to allow deletions to 3 | -- cascade. You will need to apply this update if you wish to use 4 | -- the automated cleaning service with an existing SQLite-based 5 | -- installation which has been run without a separate output store. 6 | -- 7 | -- Backing up the database is recommended before running this script. 8 | 9 | BEGIN TRANSACTION; 10 | 11 | ALTER TABLE joboutput RENAME TO joboutput_old; 12 | 13 | CREATE TABLE joboutput ( 14 | id INTEGER PRIMARY KEY AUTOINCREMENT, 15 | finishid INTEGER NOT NULL, 16 | stdout TEXT DEFAULT "" NOT NULL, 17 | stderr TEXT DEFAULT "" NOT NULL, 18 | 19 | UNIQUE (finishid), 20 | FOREIGN KEY (finishid) REFERENCES jobfinish(id) 21 | ON DELETE CASCADE ON UPDATE RESTRICT 22 | ); 23 | 24 | INSERT INTO joboutput 25 | (finishid, stdout, stderr) 26 | SELECT finishid, stdout, stderr 27 | FROM joboutput_old 28 | ORDER BY id ASC; 29 | 30 | DROP TABLE joboutput_old; 31 | 32 | COMMIT; 33 | --------------------------------------------------------------------------------