The operator, reviewing SNMP on #5214: "this system looks incomplete like there's something missing from it". The one device shown was plugin.yaml's example (core-switch 192.168.1.1, community public), polled every minute; devices could only be defined by editing YAML in the image; a failed poll was logged at DEBUG and stored nowhere, so the page said "waiting for the next poll" forever; v3 ran as v2c; counters showed as lifetime totals. - Devices are rows (snmp_devices, migration snmp_001): address, port, v1/v2c community or a v3 user with auth/priv protocols and keys (secrets encrypted), presets plus custom readings, an optional host binding, and the newest poll's outcome. The YAML device list and its example are gone. - The SNMP page adds, edits and deletes them (admin, audited, with get_audit_entities so the audit log names and links a device). Readings come from presets (system uptime, interface traffic by number, UPS-MIB, Linux net-snmp) and custom "OID name [unit]" lines. - One GET per device with every OID, devices polled at once. The outcome is written to the row in words ("No reply from … within 5 s. Check the address and port, that SNMP is on, and the community"), with readings it lacks. v1's whole-request error drops that OID and asks again. SNMPv3 signs in with USM (MD5/SHA-1/SHA-2, DES/3DES/AES). - Test asks the unsaved form's device for its name, description, uptime and interfaces by number. It is an admin's explicit action; every page reads only stored rows (System 178's ruling). - Each reading has a unit; a counter is stored as a per-second rate (bytes as bit/s), a reset or wrap skipped. The page, widget and host panel say values in units and why a device is silent; the device page draws a chart per reading on a dated axis. Tests: readings and presets, the form and its secrets, v3 checks, rates, the poller against a stand-in pysnmp (one GET, v1 retry, timeout wording, USM), the pages, and an integration test of the poll writing outcomes and rates on Postgres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
56 lines
3.0 KiB
HTML
56 lines
3.0 KiB
HTML
{# plugins/snmp/templates/snmp/index.html — every SNMP device, those not
|
|
answering first, each saying what its newest poll found: its readings in
|
|
their units, or why it didn't answer (milestone 491). #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import page_header, status_badge, stat_tile, empty_state %}
|
|
{% block title %}SNMP — Steward{% endblock %}
|
|
{% block content %}
|
|
{% set admin = session.user_role == 'admin' %}
|
|
{% call page_header("SNMP devices", (devices|length ~ " device" ~ ("s" if devices|length != 1 else "")) if devices else none) %}
|
|
{% if admin %}<a class="btn btn-sm" href="/plugins/snmp/devices/new">Add device</a>{% endif %}
|
|
{% endcall %}
|
|
|
|
{% if not devices %}
|
|
<div class="card">{{ empty_state("No SNMP devices yet. Add a switch, a UPS, a printer or a server that answers SNMP, and Steward polls it every minute.",
|
|
"Add device" if admin else none, "/plugins/snmp/devices/new") }}</div>
|
|
{% endif %}
|
|
|
|
{% for d in devices %}
|
|
<section class="card" aria-labelledby="snmp-{{ d.id }}">
|
|
<div class="host-health-head">
|
|
{% if d.reach == 'up' %}{{ status_badge('up', 'Answering') }}
|
|
{% elif d.reach == 'down' %}{{ status_badge('down', 'Not answering') }}
|
|
{% elif d.reach == 'stale' %}{{ status_badge('stale', 'Not polled') }}
|
|
{% else %}{{ status_badge('pending', 'Not polled yet') }}{% endif %}
|
|
<h2 id="snmp-{{ d.id }}" class="section-title" style="margin:0;"><a href="/plugins/snmp/devices/{{ d.id }}">{{ d.name }}</a></h2>
|
|
<span class="field-hint mono">{{ d.address }}{% if d.port != 161 %}:{{ d.port }}{% endif %}</span>
|
|
<span class="field-hint">SNMP v{{ d.version }}</span>
|
|
<span class="page-actions" style="margin-left:auto;">
|
|
<a href="/plugins/snmp/devices/{{ d.id }}" class="btn btn-sm btn-ghost">History</a>
|
|
{% if admin %}<a href="/plugins/snmp/devices/{{ d.id }}/edit" class="btn btn-sm btn-ghost">Edit</a>{% endif %}
|
|
</span>
|
|
</div>
|
|
|
|
{% if d.reach == 'down' %}
|
|
<p class="tone-crit" style="margin:0 0 0.4rem;">{{ d.says }}</p>
|
|
<p class="field-hint" style="margin:0 0 0.6rem;">
|
|
{%- if d.ok_at %}Last answered {{ d.ok_at|ago }}; the readings below are from then.{% else %}It has never answered.{% endif %}
|
|
Tried {{ d.polled_at|ago }}.</p>
|
|
{% elif d.reach == 'stale' %}
|
|
<p class="field-hint" style="margin:0 0 0.6rem;">{{ d.says }} Last polled {{ d.polled_at|ago }}.</p>
|
|
{% elif d.reach == 'pending' %}
|
|
<p class="field-hint" style="margin:0 0 0.6rem;">Not polled yet: the first poll runs within a minute of saving.</p>
|
|
{% endif %}
|
|
|
|
{% if d.reach != 'pending' and d.readings %}
|
|
<div class="stat-strip" style="margin-bottom:0;">
|
|
{% for r in d.readings %}{{ stat_tile(r.name, r.text, hint="from the second poll" if r.counter and r.text == "—" else none) }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% if d.missing %}
|
|
<p class="field-hint" style="margin:0.6rem 0 0;">Not answered by this device: {{ d.missing|join(", ") }}. It may not support them; Edit to remove them.</p>
|
|
{% endif %}
|
|
</section>
|
|
{% endfor %}
|
|
{% endblock %}
|