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>
167 lines
8.9 KiB
HTML
167 lines
8.9 KiB
HTML
{# plugins/snmp/templates/snmp/form.html — add or edit an SNMP device
|
|
(milestone 491): where it is, how to sign in, what to read, and Test to
|
|
ask the device before saving. Secrets are never sent back to the browser;
|
|
a blank secret field keeps the stored one. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, page_header %}
|
|
{% block title %}{{ ("Edit " ~ device.name) if device else "Add SNMP device" }} — Steward{% endblock %}
|
|
{% block breadcrumb %}
|
|
{%- if device -%}
|
|
{{ crumbs([("SNMP", "/plugins/snmp/"), (device.name, "/plugins/snmp/devices/" ~ device.id), ("Edit", "")]) }}
|
|
{%- else -%}
|
|
{{ crumbs([("SNMP", "/plugins/snmp/"), ("Add device", "")]) }}
|
|
{%- endif -%}
|
|
{% endblock %}
|
|
{% block content %}
|
|
{{ page_header(("Edit " ~ device.name) if device else "Add SNMP device") }}
|
|
|
|
<div class="form-page">
|
|
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
|
|
<form method="post" id="snmp-form" action="{{ '/plugins/snmp/devices/' ~ device.id ~ '/edit' if device else '/plugins/snmp/devices/new' }}">{{ csrf_field() }}
|
|
{% if device %}<input type="hidden" name="device_id" value="{{ device.id }}">{% endif %}
|
|
|
|
<section class="card form-section" aria-labelledby="snmp-where">
|
|
<h2 id="snmp-where" class="section-title">Device</h2>
|
|
<div class="form-pair">
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-name">Name</label>
|
|
<input class="input" id="snmp-name" name="name" required maxlength="100" value="{{ v.name or '' }}" placeholder="core-switch" autocomplete="off">
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-address">Address</label>
|
|
<input class="input mono" id="snmp-address" name="address" required value="{{ v.address or '' }}" placeholder="192.168.1.2 or switch.lan" autocomplete="off">
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-port">Port</label>
|
|
<input class="input" id="snmp-port" name="port" type="number" min="1" max="65535" value="{{ v.port or 161 }}">
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-host">Show on host</label>
|
|
<select class="input" id="snmp-host" name="host_id">
|
|
<option value="">The host with this address, if any</option>
|
|
{% for h in hosts %}<option value="{{ h.id }}"{% if v.host_id == h.id %} selected{% endif %}>{{ h.name }}</option>{% endfor %}
|
|
</select>
|
|
<span class="field-hint">Which host's page shows this device's readings.</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card form-section" aria-labelledby="snmp-auth">
|
|
<h2 id="snmp-auth" class="section-title">Sign-in</h2>
|
|
<div class="form-pair">
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-version">SNMP version</label>
|
|
<select class="input" id="snmp-version" name="version">
|
|
{% for ver, words in [("2c", "v2c: a community string (most devices)"), ("3", "v3: a user with authentication and encryption"), ("1", "v1: a community string (old devices)")] %}
|
|
<option value="{{ ver }}"{% if v.version == ver %} selected{% endif %}>{{ words }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field" data-snmp="community">
|
|
<label class="field-label" for="snmp-community">Community</label>
|
|
<input class="input" id="snmp-community" name="community" type="password" autocomplete="new-password"
|
|
placeholder="{{ 'kept; type to change' if v.has_community else 'public' }}">
|
|
<span class="field-hint">The device's read community. Blank {{ "keeps the saved one" if v.has_community else "means public" }}.</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-pair" data-snmp="v3">
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-user">User</label>
|
|
<input class="input" id="snmp-user" name="v3_user" value="{{ v.v3_user or '' }}" autocomplete="off">
|
|
</div>
|
|
<div class="field"></div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-auth-proto">Authentication</label>
|
|
<select class="input" id="snmp-auth-proto" name="v3_auth">
|
|
{% for val, words in [("sha", "SHA-1"), ("sha256", "SHA-256"), ("sha512", "SHA-512"), ("sha224", "SHA-224"), ("sha384", "SHA-384"), ("md5", "MD5"), ("none", "None")] %}
|
|
<option value="{{ val }}"{% if v.v3_auth == val %} selected{% endif %}>{{ words }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-auth-key">Authentication key</label>
|
|
<input class="input" id="snmp-auth-key" name="v3_auth_key" type="password" autocomplete="new-password"
|
|
placeholder="{{ 'kept; type to change' if v.has_auth_key else 'at least 8 characters' }}">
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-priv-proto">Encryption</label>
|
|
<select class="input" id="snmp-priv-proto" name="v3_priv">
|
|
{% for val, words in [("aes128", "AES-128"), ("aes192", "AES-192"), ("aes256", "AES-256"), ("des", "DES"), ("3des", "3DES"), ("none", "None")] %}
|
|
<option value="{{ val }}"{% if v.v3_priv == val %} selected{% endif %}>{{ words }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label class="field-label" for="snmp-priv-key">Encryption key</label>
|
|
<input class="input" id="snmp-priv-key" name="v3_priv_key" type="password" autocomplete="new-password"
|
|
placeholder="{{ 'kept; type to change' if v.has_priv_key else 'at least 8 characters' }}">
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
|
|
<button type="button" class="btn btn-sm btn-ghost"
|
|
hx-post="/plugins/snmp/devices/test" hx-include="#snmp-form" hx-target="#snmp-test"
|
|
hx-indicator="#snmp-testing">Test</button>
|
|
<span id="snmp-testing" class="field-hint htmx-indicator">Asking the device…</span>
|
|
<span class="field-hint">Asks the device for its name, uptime and interfaces, with what's typed here. Nothing is saved.</span>
|
|
</div>
|
|
<div id="snmp-test" aria-live="polite"></div>
|
|
</section>
|
|
|
|
<section class="card form-section" aria-labelledby="snmp-what">
|
|
<h2 id="snmp-what" class="section-title">Readings</h2>
|
|
<div class="check-list">
|
|
{% for pid, label, what in presets %}
|
|
<label style="display:flex;gap:0.5rem;align-items:flex-start;cursor:pointer;">
|
|
<input type="checkbox" name="presets" value="{{ pid }}"{% if pid in (v.presets or []) %} checked{% endif %} style="margin-top:0.2rem;">
|
|
<span><strong>{{ label }}</strong> <span class="field-hint">{{ what }}</span></span>
|
|
</label>
|
|
{% if pid == "interfaces" %}
|
|
<div class="field" style="margin:0 0 0.4rem 1.6rem;">
|
|
<label class="field-label" for="snmp-interfaces">Interface numbers</label>
|
|
<input class="input" id="snmp-interfaces" name="interfaces" value="{{ v.interfaces or '' }}" placeholder="1, 2, 5-8" autocomplete="off" style="max-width:20rem;">
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
<div class="field" style="margin-top:0.9rem;">
|
|
<label class="field-label" for="snmp-custom">Custom readings</label>
|
|
<textarea class="input mono" id="snmp-custom" name="custom" rows="4" placeholder="1.3.6.1.4.1.318.1.1.1.2.2.2.0 battery_temperature celsius">{{ v.custom or '' }}</textarea>
|
|
<span class="field-hint">One a line: the OID, a name (letters, digits, underscores), and optionally a unit:
|
|
{% for u, words in units.items() %}<span class="mono">{{ u }}</span> ({{ words }}){{ ", " if not loop.last else "." }}{% endfor %}</span>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn">{{ "Save" if device else "Add device" }}</button>
|
|
<a href="{{ '/plugins/snmp/devices/' ~ device.id if device else '/plugins/snmp/' }}" class="btn btn-ghost">Cancel</a>
|
|
</div>
|
|
</form>
|
|
|
|
{% if device %}
|
|
<section class="card danger-zone" aria-labelledby="snmp-delete">
|
|
<div>
|
|
<h2 id="snmp-delete" class="section-title">Delete this device</h2>
|
|
<p class="field-hint">Steward stops polling it. Its past readings age out with the rest of the history.</p>
|
|
</div>
|
|
<form method="post" action="/plugins/snmp/devices/{{ device.id }}/delete"
|
|
data-confirm="Delete SNMP device {{ device.name }}? Steward stops polling it.">{{ csrf_field() }}
|
|
<button class="btn btn-danger" type="submit">Delete device</button>
|
|
</form>
|
|
</section>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
var version = document.getElementById("snmp-version");
|
|
function show() {
|
|
var v3 = version.value === "3";
|
|
document.querySelectorAll('[data-snmp="v3"]').forEach(function (el) { el.hidden = !v3; });
|
|
document.querySelectorAll('[data-snmp="community"]').forEach(function (el) { el.hidden = v3; });
|
|
}
|
|
version.addEventListener("change", show);
|
|
show();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|