Rename application to Samsara and add assessment summary, detailed tabs and local Linux inventory
This commit is contained in:
95
samsara/collectors/linux_schedules.py
Normal file
95
samsara/collectors/linux_schedules.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""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']
|
||||
Reference in New Issue
Block a user