fix(syslog): word its audit actions, list its tables for backups, mark its page top-level, reach its modules as loaded (#5999)
- Audit: syslog.source_* worded, and a Syslog filter area. - Backups keep syslog_sources (added, named, ignored; what it last sent isn't compared) and leave syslog_lines out, as logs. - The integration test reaches the plugin as loaded (#5775), so its tables aren't defined twice; ruff E741. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,11 @@ ACTIONS = {
|
|||||||
"prometheus.template_created": "added Prometheus widget template",
|
"prometheus.template_created": "added Prometheus widget template",
|
||||||
"prometheus.template_updated": "changed Prometheus widget template",
|
"prometheus.template_updated": "changed Prometheus widget template",
|
||||||
"prometheus.template_deleted": "deleted Prometheus widget template",
|
"prometheus.template_deleted": "deleted Prometheus widget template",
|
||||||
|
"syslog.source_added": "added syslog source",
|
||||||
|
"syslog.source_updated": "changed syslog source",
|
||||||
|
"syslog.source_removed": "removed syslog source",
|
||||||
|
"syslog.source_ignored": "ignored syslog sender",
|
||||||
|
"syslog.source_offered": "offered again syslog sender",
|
||||||
"backup.made": "sent a backup",
|
"backup.made": "sent a backup",
|
||||||
"backup.failed": "tried to send a backup, which failed:",
|
"backup.failed": "tried to send a backup, which failed:",
|
||||||
"backup.restored": "restored Steward from a backup",
|
"backup.restored": "restored Steward from a backup",
|
||||||
@@ -210,7 +215,7 @@ AREAS = (("auth.", "Sign-ins"), ("account.", "Accounts"), ("user.", "Users"), ("
|
|||||||
("ansible.run", "Playbook runs"), ("ansible.", "Ansible (all)"),
|
("ansible.run", "Playbook runs"), ("ansible.", "Ansible (all)"),
|
||||||
("settings.", "Settings"), ("plugin.", "Plugins"), ("api_token.", "AI access"),
|
("settings.", "Settings"), ("plugin.", "Plugins"), ("api_token.", "AI access"),
|
||||||
("docker.", "Docker"), ("snmp.", "SNMP"), ("prometheus.", "Prometheus"),
|
("docker.", "Docker"), ("snmp.", "SNMP"), ("prometheus.", "Prometheus"),
|
||||||
("backup.", "Backups"))
|
("syslog.", "Syslog"), ("backup.", "Backups"))
|
||||||
|
|
||||||
|
|
||||||
def action_text(action: str) -> str:
|
def action_text(action: str) -> str:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ KEPT: frozenset[str] = frozenset({
|
|||||||
"snmp_devices", "prometheus_targets", "prometheus_widget_templates",
|
"snmp_devices", "prometheus_targets", "prometheus_widget_templates",
|
||||||
# Names given to network clients by hand (UniFi's alias column).
|
# Names given to network clients by hand (UniFi's alias column).
|
||||||
"unifi_known_clients",
|
"unifi_known_clients",
|
||||||
|
# Syslog senders: which were added, under what name, and which ignored.
|
||||||
|
"syslog_sources",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Rows that are measurements, logs, history or in-flight work: left out.
|
# Rows that are measurements, logs, history or in-flight work: left out.
|
||||||
@@ -58,6 +60,7 @@ EXCLUDED: frozenset[str] = frozenset({
|
|||||||
"unifi_alarms", "unifi_client_snapshots", "unifi_devices", "unifi_dpi_snapshots",
|
"unifi_alarms", "unifi_client_snapshots", "unifi_devices", "unifi_dpi_snapshots",
|
||||||
"unifi_events", "unifi_fw_rules", "unifi_networks", "unifi_port_forwards",
|
"unifi_events", "unifi_fw_rules", "unifi_networks", "unifi_port_forwards",
|
||||||
"unifi_speedtest_results", "unifi_wan_stats", "unifi_wlan_stats",
|
"unifi_speedtest_results", "unifi_wan_stats", "unifi_wlan_stats",
|
||||||
|
"syslog_lines",
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── What a check compares (#5823) ────────────────────────────────────────────
|
# ── What a check compares (#5823) ────────────────────────────────────────────
|
||||||
@@ -82,6 +85,9 @@ ACTIVITY: dict[str, frozenset[str]] = {
|
|||||||
"snmp_devices": frozenset({"last_polled_at", "last_ok_at", "last_error", "missing"}),
|
"snmp_devices": frozenset({"last_polled_at", "last_ok_at", "last_error", "missing"}),
|
||||||
"prometheus_targets": frozenset({"last_scraped_at", "last_ok_at", "last_error", "last_kept",
|
"prometheus_targets": frozenset({"last_scraped_at", "last_ok_at", "last_error", "last_kept",
|
||||||
"last_dropped", "last_capped"}),
|
"last_dropped", "last_capped"}),
|
||||||
|
# What the sender last sent; its address, name, state and host are setup.
|
||||||
|
"syslog_sources": frozenset({"hostname_seen", "first_seen_at", "last_seen_at", "lines_total",
|
||||||
|
"last_severity", "last_line"}),
|
||||||
# Scraped from the controller; the name and note are what someone set.
|
# Scraped from the controller; the name and note are what someone set.
|
||||||
"unifi_known_clients": frozenset({"hostname", "ip", "mac_vendor", "is_blocked", "first_seen",
|
"unifi_known_clients": frozenset({"hostname", "ip", "mac_vendor", "is_blocked", "first_seen",
|
||||||
"last_seen", "scraped_at"}),
|
"last_seen", "scraped_at"}),
|
||||||
@@ -109,5 +115,5 @@ AREAS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|||||||
("Ansible", ("ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules")),
|
("Ansible", ("ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules")),
|
||||||
("Dashboards", ("dashboards", "dashboard_widgets", "dashboard_share_tokens")),
|
("Dashboards", ("dashboards", "dashboard_widgets", "dashboard_share_tokens")),
|
||||||
("Plugins", ("snmp_devices", "prometheus_targets", "prometheus_widget_templates",
|
("Plugins", ("snmp_devices", "prometheus_targets", "prometheus_widget_templates",
|
||||||
"unifi_known_clients")),
|
"unifi_known_clients", "syslog_sources")),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,17 +20,25 @@ def app():
|
|||||||
return create_app(testing=False)
|
return create_app(testing=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _mod(sub):
|
||||||
|
"""The loaded plugin's module: importing plugins.syslog.models by path
|
||||||
|
would define its tables a second time (#5775)."""
|
||||||
|
from steward.core.plugin_manager import loaded_plugin_submodule
|
||||||
|
module = loaded_plugin_submodule("syslog", sub)
|
||||||
|
assert module is not None, "the syslog plugin isn't loaded"
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
def _received(address, text, at):
|
def _received(address, text, at):
|
||||||
from plugins.syslog.parse import parse
|
receiver = _mod("receiver")
|
||||||
from plugins.syslog.receiver import Received
|
return receiver.Received(at, address, _mod("parse").parse(f"<11>Oct 19 01:12:03 synnas kernel: {text}"))
|
||||||
return Received(at, address, parse(f"<11>Oct 19 01:12:03 synnas kernel: {text}"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_an_unknown_sender_is_offered_and_an_added_one_kept(app):
|
def test_an_unknown_sender_is_offered_and_an_added_one_kept(app):
|
||||||
from sqlalchemy import delete, func, select
|
from sqlalchemy import delete, func, select
|
||||||
|
|
||||||
from plugins.syslog.models import SyslogLine, SyslogSource
|
SyslogLine, SyslogSource = _mod("models").SyslogLine, _mod("models").SyslogSource
|
||||||
from plugins.syslog.receiver import store_lines
|
store_lines = _mod("receiver").store_lines
|
||||||
|
|
||||||
now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc)
|
now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
@@ -54,7 +62,7 @@ def test_an_unknown_sender_is_offered_and_an_added_one_kept(app):
|
|||||||
await store_lines(s, [_received("10.9.0.1", "kept", now + timedelta(seconds=2))])
|
await store_lines(s, [_received("10.9.0.1", "kept", now + timedelta(seconds=2))])
|
||||||
async with app.db_sessionmaker() as s:
|
async with app.db_sessionmaker() as s:
|
||||||
lines = (await s.execute(select(SyslogLine))).scalars().all()
|
lines = (await s.execute(select(SyslogLine))).scalars().all()
|
||||||
assert [(l.app, l.hostname, l.message, l.severity) for l in lines] == [("kernel", "synnas", "kept", 3)]
|
assert [(ln.app, ln.hostname, ln.message, ln.severity) for ln in lines] == [("kernel", "synnas", "kept", 3)]
|
||||||
assert (await s.get(SyslogSource, src_id)).lines_total == 3
|
assert (await s.get(SyslogSource, src_id)).lines_total == 3
|
||||||
async with s.begin():
|
async with s.begin():
|
||||||
await s.execute(delete(SyslogSource))
|
await s.execute(delete(SyslogSource))
|
||||||
@@ -65,8 +73,8 @@ def test_an_unknown_sender_is_offered_and_an_added_one_kept(app):
|
|||||||
def test_the_ring_keeps_each_source_within_its_window_and_cap(app):
|
def test_the_ring_keeps_each_source_within_its_window_and_cap(app):
|
||||||
from sqlalchemy import delete, select
|
from sqlalchemy import delete, select
|
||||||
|
|
||||||
from plugins.syslog.models import SyslogLine, SyslogSource
|
SyslogLine, SyslogSource = _mod("models").SyslogLine, _mod("models").SyslogSource
|
||||||
from plugins.syslog.retention import prune_syslog_lines
|
prune_syslog_lines = _mod("retention").prune_syslog_lines
|
||||||
|
|
||||||
now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc)
|
now = datetime(2026, 10, 19, 3, 0, tzinfo=timezone.utc)
|
||||||
line40 = "x" * 40 # cap 100 keeps 3 (prefixes 0, 40, 80)
|
line40 = "x" * 40 # cap 100 keeps 3 (prefixes 0, 40, 80)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async def test_a_viewer_sees_sources_but_no_controls(app):
|
|||||||
async def test_a_port_it_cannot_open_is_said(app):
|
async def test_a_port_it_cannot_open_is_said(app):
|
||||||
page = await _render(app, "admin", listener=_listener(
|
page = await _render(app, "admin", listener=_listener(
|
||||||
listening=False, error="Port 514 couldn't be opened (UDP: Permission denied)."))
|
listening=False, error="Port 514 couldn't be opened (UDP: Permission denied)."))
|
||||||
assert "Not listening" in page and "Port 514 couldn't be opened" in page
|
assert "Not listening" in page and "Port 514 couldn't be opened" in page
|
||||||
assert "below 1024" in page
|
assert "below 1024" in page
|
||||||
assert "No sources yet." in page
|
assert "No sources yet." in page
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ TOP_LEVEL = {
|
|||||||
"plugins/prometheus/templates/prometheus/index.html",
|
"plugins/prometheus/templates/prometheus/index.html",
|
||||||
"plugins/traefik/templates/traefik/index.html",
|
"plugins/traefik/templates/traefik/index.html",
|
||||||
"plugins/unifi/templates/unifi/index.html",
|
"plugins/unifi/templates/unifi/index.html",
|
||||||
|
"plugins/syslog/templates/syslog/index.html",
|
||||||
# Logged out, or no page to go back up from.
|
# Logged out, or no page to go back up from.
|
||||||
"steward/templates/auth/login.html",
|
"steward/templates/auth/login.html",
|
||||||
"steward/templates/auth/setup.html",
|
"steward/templates/auth/setup.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user