import re
from insights.core import CommandParser
from insights.core.exceptions import ParseException
from insights.core.plugins import parser
from insights.parsers import get_active_lines
from insights.specs import Specs
def _make_cron_re():
r"""
Make the regular expression that matches a crontab 'cron' line.
Each field has a set of allowed values, and can then be in a range, and be
listed with dashes. A range can be stepped with the '/' modifier, and
ranges can be in a list. A field can also be '*', or '*' divided in steps.
The best way to do this is to have a template for a single field that
encapsulates the syntax of that field, regardless of what that field
matches. We then fill in the actual template's value with the pattern
that matches that field. Each field is named, so we can pull them out as
a dictionary later.
"""
range_ = r"{val}(?:-{val}(?:/\d+)?)?"
template = r"(?P<{name}>" + r"(?:\*(?:/\d+)?|{r}(?:,{r})*)".format(r=range_) + r")\s+"
return (
r'^\s*' +
template.format(name='minute', val=r'(?:\d|[012345]\d)') +
template.format(name='hour', val=r'(?:\d|[01]\d|2[0123])') +
template.format(name='day_of_month', val=r'(?:0?[1-9]|[12]\d|3[01])') +
template.format(name='month', val=r'(?:0?[1-9]|1[012]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)') +
template.format(name='day_of_week', val=r'(?:[0-7]|mon|tue|wed|thur|fri|sat|sun)') +
r'(?P<command>\S.*)$'
)
[docs]
class CrontabL(CommandParser):
r"""
Parses output of ``crontab -l`` command.
Each row of the crontab is converted into a dictionary with keys for each field.
For example one row would look like::
{
'minute': '*',
'hour': '*',
'day_of_month': '*',
'month': '*',
'day_of_week': '*',
'command': '/usr/bin/keystone-manage token_flush > /dev/null 2>&1'
}
Crontab parses the line in the same way that cron(1) does. Lines that
are blank or start with a comment are ignored. Environment lines of the
form 'KEY = value' (with optional spacing around the equals sign) are
stored in the 'environment' dictionary attribute by key. Lines
containing a valid crontab line, with five recogniseable time fields and
a command, are stored in the `data` property and accessed through the
pseudo-list interface and search method. All other lines are stored in
the `invalid_lines` property.
Crontab recognises the extension of time signature 'nicknames', which
take place of the first five parts of a standard crontab line:
* `@reboot` : Run once after reboot.
* `@yearly` : Run once a year, ie. "0 0 1 1 \*".
* `@annually` : Run once a year, ie. "0 0 1 1 \*".
* `@monthly` : Run once a month, ie. "0 0 1 \* \*".
* `@weekly` : Run once a week, ie. "0 0 \* \* 0".
* `@daily` : Run once a day, ie. "0 0 \* \* \*".
* `@hourly` : Run once an hour, ie. "0 \* \* \* \*".
The Crontab class recognises these nicknames. In the case of the '@reboot'
nickname, the row will not contain the 'minute', 'hour', 'day_of_month',
'month', or 'day_of_week' keys, and instead will contain the key 'time' with
the value '@reboot' (as well as the usual 'command' key). All other keywords
are translated directly into their five-part equivalent and parsed as a normal
crontab line.
Lines that can't be parsed because they don't contain at least six words or
meet the criteria for environment line or time signature nicknames are stored
in the ``invalid_lines`` property.
Sample input looks like::
# send mail to admin address.
MAILTO=admin
* * * * * /usr/bin/keystone-manage token_flush > /dev/null 2>&1
33 0 * * * /bin/heat-manage purge_deleted -g days 7
Examples:
>>> crontab = shared[KeystoneCrontab]
>>> crontab.search('keystone')
[{'minute': '*', 'hour': '*', 'day_of_month': '*', 'month': '*', 'day_of_week': '*',
'command': '/usr/bin/keystone-manage token_flush > /dev/null 2>&1'}]
>>> [r['minute'] for r in crontab]
['*', '33']
>>> crontab.invalid_lines
[]
>>> len(crontab) # Number of actual entries
2
>>> crontab[0] # Individual entry access
{'minute': '*', 'hour': '*', 'day_of_month': '*', 'month': '*', 'day_of_week': '*',
'command': '/usr/bin/keystone-manage token_flush > /dev/null 2>&1'}
>>> 'MAILTO' in crontab.environment # Dictionary of environment settings
True
>>> crontab.environment['MAILTO']
'admin'
Attributes:
data(list): List of parsed lines. These can be accessed directly through
the object as seen in the examples.
environment(dict): A dictionary of environment declarations in the crontab.
invalid_lines(list): Lines that could not be parsed as normal crontab
entries.
"""
[docs]
def parse_content(self, content):
self.data = []
self.environment = {}
self.invalid_lines = []
# Crontabs can use 'nicknames' for common event frequencies:
nicknames = {
'@yearly': '0 0 1 1 *',
'@annually': '0 0 1 1 *',
'@monthly': '0 0 1 * *',
'@weekly': '0 0 * * 0',
'@daily': '0 0 * * *',
'@hourly': '0 * * * *',
}
cron_re = re.compile(_make_cron_re(), flags=re.IGNORECASE)
env_re = re.compile(r'^\s*(?P<key>\w+)\s*=\s*(?P<value>\S.*)$')
for line in get_active_lines(content):
if line.startswith('@'):
# Reboot is 'special':
if line.startswith('@reboot'):
parts = line.split(None, 2)
self.data.append({'time': '@reboot', 'command': parts[1]})
continue
else:
parts = line.split(None, 2)
if parts[0] not in nicknames:
raise ParseException(
"{n} not recognised as a time specification 'nickname'".format(n=parts[0])
)
# Otherwise, put the time spec nickname translation into
# the line
line = line.replace(parts[0], nicknames[parts[0]])
# And then we fall through to the rest of the parsing.
cron_match = cron_re.match(line)
env_match = env_re.match(line)
if cron_match:
self.data.append(cron_match.groupdict())
elif env_match:
# Environment variable - capture in dictionary
self.environment[env_match.group('key')] = env_match.group('value')
else:
self.invalid_lines.append(line)
def __getitem__(self, idx):
"""
Get the n'th item from the crontab table.
Arguments:
idx(int): The index of the crontab line, starting from 0.
Returns:
(dict): the number of items
"""
return self.data[idx]
def __iter__(self):
"""
Iterates through the list of crontab entries.
Yields:
(dict): The next parsed crontab entry.
"""
for row in self.data:
yield row
[docs]
def search(self, filter_str):
"""list: Returns list of dicts for lines that have `filter_str` in
the command."""
return [r for r in self.data if filter_str in r['command']]
[docs]
@parser(Specs.heat_crontab)
class HeatCrontab(CrontabL):
"""Parses output of the ``crontab -l -u heat`` command."""
pass
[docs]
@parser(Specs.keystone_crontab)
class KeystoneCrontab(CrontabL):
"""Parses output of the ``crontab -l -u keystone`` command."""
pass
@parser(Specs.nova_crontab)
class NovaCrontab(CrontabL):
"""Parses output of the ``crontab -l -u nova`` command."""
pass
[docs]
@parser(Specs.root_crontab)
class RootCrontab(CrontabL):
"""Parses output of the ``crontab -l -u root`` command."""
pass