Files
Samsara/samsara/collectors/linux_schedules.py

96 lines
4.1 KiB
Python

"""Read scheduler metadata only; never run jobs or report their arguments."""
from pathlib import Path
import re
import shlex
SAFE_PATH = re.compile(r'^[A-Za-z0-9_./+-]+$')
def cron_rows(text, source, system_file=False):
rows = []
for number, raw in enumerate(text.splitlines(), 1):
line = raw.strip()
if not line or line.startswith('#') or re.match(r'^[A-Za-z_][A-Za-z0-9_]*\s*=', line):
continue
parts = line.split(None, 1 if line.startswith('@') else 5)
count = 1 if line.startswith('@') else 5
if count == 1 and parts[0] not in {'@reboot', '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@midnight', '@hourly'}:
continue
if len(parts) <= count:
continue
schedule, command = ' '.join(parts[:count]), parts[count]
account = 'Current user'
if system_file:
user_command = command.split(None, 1)
if len(user_command) != 2:
continue
account, command = user_command
# A safe executable/script filename is enough to identify the workload.
# Arguments, embedded scripts and environment assignments never leave here.
try:
tokens = shlex.split(command)
except ValueError:
tokens = []
executable = tokens[0] if tokens and SAFE_PATH.fullmatch(tokens[0]) else '[command omitted]'
script = executable
if Path(executable).name.startswith(('python', 'perl', 'ruby', 'bash', 'sh')) and len(tokens) > 1:
if SAFE_PATH.fullmatch(tokens[1]) and not tokens[1].startswith('-'):
script = tokens[1]
rows.append({'name': f'{Path(script).name} ({source}:{number})', 'path': source,
'enabled': True, 'account': account, 'executable': executable,
'triggers': schedule + ' (cron; timezone not resolved)',
'state': 'Configured schedule; execution unverified'})
return rows
def read_system_cron(cancelled=lambda: False):
paths = [Path('/etc/crontab')]
directory = Path('/etc/cron.d')
if directory.exists():
paths.extend(sorted(directory.iterdir()))
rows, incomplete = [], False
incomplete = len(paths) > 256
for path in paths[:256]:
if cancelled():
from samsara.collectors.linux import CollectionCancelled
raise CollectionCancelled()
if not path.exists():
continue
# Avoid following symlinks or special files. Bound every file read.
if path.is_symlink() or not path.is_file():
incomplete = True
continue
if path.name.startswith('.') or path.name.endswith(('~', '.bak', '.dpkg-old', '.rpmnew', '.rpmsave')):
continue
try:
with path.open('r', encoding='utf-8', errors='replace') as source:
text = source.read(65537)
if len(text) > 65536:
incomplete = True
continue
rows.extend(cron_rows(text, str(path), system_file=True))
except OSError:
incomplete = True
return rows, incomplete
def enrich_timers(rows, command):
names = [row['name'] for row in rows if re.fullmatch(r'[A-Za-z0-9_.@:-]+\.timer', row['name'])]
if not names:
return
output = command(['systemctl', 'show', '--property=Id', '--property=TimersCalendar',
'--property=NextElapseUSecRealtime', '--property=Triggers', '--no-pager', '--', *names[:200]])
details = {}
for block in output.strip().split('\n\n'):
properties = dict(line.split('=', 1) for line in block.splitlines() if '=' in line)
if 'Id' in properties:
details[properties['Id']] = properties
for row in rows:
properties = details.get(row['name'], {})
schedule = properties.get('TimersCalendar', '')
next_run = properties.get('NextElapseUSecRealtime', '')
if schedule or next_run:
row['triggers'] = '; '.join(filter(None, [schedule, 'Next: ' + next_run if next_run else '']))
if properties.get('Triggers'):
row['associated_unit'] = properties['Triggers']