mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Accept ping alias for availability targets
This commit is contained in:
parent
597c369a63
commit
4eac3b535c
7 changed files with 183 additions and 4 deletions
46
docs/API.md
46
docs/API.md
|
|
@ -1264,6 +1264,52 @@ These routes manage Docker / Podman telemetry and container actions reported by
|
|||
|
||||
---
|
||||
|
||||
## Availability Checks
|
||||
|
||||
Agentless availability checks monitor endpoint-only devices and services with
|
||||
ICMP ping, TCP port, HTTP, or HTTPS probes. They are managed from
|
||||
**Settings -> Monitoring -> Availability checks** and are also exposed through
|
||||
the API for automation.
|
||||
|
||||
### Target Management
|
||||
|
||||
- `GET /api/availability-targets` (`settings:read`) - List configured targets and latest probe status.
|
||||
- `POST /api/availability-targets` (`settings:write`) - Add a target.
|
||||
- `PUT /api/availability-targets/{id}` (`settings:write`) - Update a target.
|
||||
- `DELETE /api/availability-targets/{id}` (`settings:write`) - Remove a target.
|
||||
- `POST /api/availability-targets/test` (`settings:write`) - Test an unsaved target.
|
||||
- `POST /api/availability-targets/{id}/test` (`settings:write`) - Test a saved target.
|
||||
|
||||
Target payload fields:
|
||||
|
||||
- `name` - Display name.
|
||||
- `targetKind` - `machine`, `service`, or `device`; defaults to `service`.
|
||||
- `address` - Hostname, IP address, or URL.
|
||||
- `protocol` - `icmp`, `tcp`, `http`, or `https`. The input alias `ping` is accepted and is stored/returned as canonical `icmp`.
|
||||
- `port` - Required for `tcp`, optional for `http`/`https`, and omitted for `icmp`.
|
||||
- `path` - Optional HTTP path.
|
||||
- `enabled` - Whether the target is scheduled.
|
||||
- `pollIntervalSeconds` - Minimum 10 seconds; defaults to 60.
|
||||
- `timeoutMillis` - Minimum 250 milliseconds; defaults to 2000.
|
||||
- `failureThreshold` - Number of consecutive failures before alerting; defaults to 2.
|
||||
- `linkedResourceId` - Optional resource id hint for attaching the probe facet to an existing resource.
|
||||
|
||||
Example ping-only target:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Garage temperature sensor",
|
||||
"targetKind": "device",
|
||||
"address": "garage-sensor.local",
|
||||
"protocol": "ping",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
The create response and subsequent reads return `"protocol": "icmp"`.
|
||||
|
||||
---
|
||||
|
||||
## 🐟 TrueNAS
|
||||
|
||||
TrueNAS connection management endpoints for adding, testing, and removing TrueNAS SCALE/CORE instances.
|
||||
|
|
|
|||
|
|
@ -402,6 +402,47 @@ Pulse uses a powerful alerting engine with hysteresis (separate trigger/clear th
|
|||
|
||||
---
|
||||
|
||||
## Availability Checks (`availability_targets.enc`)
|
||||
|
||||
Availability checks are agentless probes for devices and services where Pulse
|
||||
cannot install an agent or does not need full machine telemetry. Use them for
|
||||
simple ping monitoring, TCP service checks, and HTTP/HTTPS status checks.
|
||||
|
||||
**Managed via UI**: Settings -> Monitoring -> Availability checks
|
||||
|
||||
Supported protocols:
|
||||
|
||||
| Protocol | Use case | Required fields |
|
||||
| ---------- | ---------- | ---------------- |
|
||||
| `icmp` | Ping-only reachability for devices, computers, and appliances | `address` |
|
||||
| `ping` | API input alias for `icmp`; saved targets return `icmp` | `address` |
|
||||
| `tcp` | A reachable port such as MQTT, SSH, or a custom service | `address`, `port` |
|
||||
| `http` / `https` | Web UI or health endpoint availability | `address`, optional `port`, optional `path` |
|
||||
|
||||
Saved targets include `name`, `targetKind` (`machine`, `service`, or
|
||||
`device`), `address`, `protocol`, `enabled`, polling interval, timeout,
|
||||
failure threshold, and an optional `linkedResourceId`. ICMP ping is the
|
||||
default probe for new targets. Availability targets publish
|
||||
`network-endpoint` resources and can raise downtime alerts after the configured
|
||||
failure threshold.
|
||||
|
||||
Example API payload for simple ping monitoring:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Garage temperature sensor",
|
||||
"targetKind": "device",
|
||||
"address": "garage-sensor.local",
|
||||
"protocol": "ping",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
Pulse stores and returns that target as `protocol: "icmp"` so dashboards,
|
||||
alerts, and resource projections keep one canonical protocol value.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 HTTPS / TLS
|
||||
|
||||
Enable HTTPS by providing certificate files via environment variables.
|
||||
|
|
|
|||
|
|
@ -994,6 +994,11 @@ payload shape change when the portal presents compact client rows.
|
|||
frontend-primitives' `CalloutCard` for shared settings callout chrome;
|
||||
API contracts own the target CRUD/test payload semantics and endpoint
|
||||
routing, not local colored alert shells.
|
||||
Availability target protocol vocabulary is canonicalized at the API/config
|
||||
boundary: clients may submit `ping` as a user-facing alias for the ICMP
|
||||
check, but saved targets, API reads, probe status, connections rows, and
|
||||
unified-resource availability facets must continue to use the canonical
|
||||
`icmp` value.
|
||||
Connections ledger rows with type
|
||||
`availability` must route management handoffs, pause, remove, and test
|
||||
actions to those availability-target endpoints and must not reuse node,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,52 @@ func TestAvailabilityHandlersCRUDPersistsTargets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityHandlersCreateNormalizesPingAlias(t *testing.T) {
|
||||
persistence := config.NewConfigPersistence(t.TempDir())
|
||||
handler := NewAvailabilityHandlers(
|
||||
func(_ context.Context) *config.ConfigPersistence { return persistence },
|
||||
nil,
|
||||
)
|
||||
|
||||
createBody := availabilityRequestBody(t, config.AvailabilityTarget{
|
||||
Name: "Garage sensor",
|
||||
Address: "https://garage-sensor.local/status",
|
||||
Protocol: config.AvailabilityProbeProtocol("ping"),
|
||||
Enabled: true,
|
||||
PollIntervalSecs: 30,
|
||||
TimeoutMillis: 1000,
|
||||
FailureThreshold: 2,
|
||||
})
|
||||
createReq := httptest.NewRequest(http.MethodPost, "/api/availability-targets", createBody)
|
||||
createRec := httptest.NewRecorder()
|
||||
handler.HandleAdd(createRec, createReq)
|
||||
if createRec.Code != http.StatusCreated {
|
||||
t.Fatalf("HandleAdd status = %d, body=%s", createRec.Code, createRec.Body.String())
|
||||
}
|
||||
|
||||
var created config.AvailabilityTarget
|
||||
if err := json.NewDecoder(createRec.Body).Decode(&created); err != nil {
|
||||
t.Fatalf("decode created target: %v", err)
|
||||
}
|
||||
if created.Protocol != config.AvailabilityProbeICMP {
|
||||
t.Fatalf("created Protocol = %q, want %q", created.Protocol, config.AvailabilityProbeICMP)
|
||||
}
|
||||
if created.Address != "garage-sensor.local" {
|
||||
t.Fatalf("created Address = %q, want garage-sensor.local", created.Address)
|
||||
}
|
||||
|
||||
loaded, err := persistence.LoadAvailabilityTargets()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAvailabilityTargets() error = %v", err)
|
||||
}
|
||||
if len(loaded) != 1 {
|
||||
t.Fatalf("LoadAvailabilityTargets() length = %d, want 1", len(loaded))
|
||||
}
|
||||
if loaded[0].Protocol != config.AvailabilityProbeICMP {
|
||||
t.Fatalf("persisted Protocol = %q, want %q", loaded[0].Protocol, config.AvailabilityProbeICMP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityHandlersTestSavedTarget(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ const (
|
|||
AvailabilityProbeTCP AvailabilityProbeProtocol = "tcp"
|
||||
AvailabilityProbeHTTP AvailabilityProbeProtocol = "http"
|
||||
AvailabilityProbeHTTPS AvailabilityProbeProtocol = "https"
|
||||
|
||||
availabilityProbePingAlias AvailabilityProbeProtocol = "ping"
|
||||
)
|
||||
|
||||
type AvailabilityTargetKind string
|
||||
|
|
@ -71,6 +73,8 @@ func (t *AvailabilityTarget) ApplyDefaults() {
|
|||
}
|
||||
if strings.TrimSpace(string(t.Protocol)) == "" {
|
||||
t.Protocol = AvailabilityProbeICMP
|
||||
} else {
|
||||
t.Protocol = normalizeAvailabilityProbeProtocol(t.Protocol)
|
||||
}
|
||||
if strings.TrimSpace(string(t.TargetKind)) == "" {
|
||||
t.TargetKind = AvailabilityTargetService
|
||||
|
|
@ -127,7 +131,8 @@ func (t AvailabilityTarget) Validate() error {
|
|||
default:
|
||||
return fmt.Errorf("unsupported availability target kind %q", t.TargetKind)
|
||||
}
|
||||
switch t.Protocol {
|
||||
protocol := normalizeAvailabilityProbeProtocol(t.Protocol)
|
||||
switch protocol {
|
||||
case AvailabilityProbeICMP:
|
||||
if t.Port != 0 {
|
||||
return fmt.Errorf("icmp availability targets must not set a port")
|
||||
|
|
@ -152,7 +157,7 @@ func (t AvailabilityTarget) Validate() error {
|
|||
if t.FailureThreshold > 0 && t.FailureThreshold > 10 {
|
||||
return fmt.Errorf("availability failure threshold must be 10 or less")
|
||||
}
|
||||
if t.Protocol == AvailabilityProbeHTTP || t.Protocol == AvailabilityProbeHTTPS {
|
||||
if protocol == AvailabilityProbeHTTP || protocol == AvailabilityProbeHTTPS {
|
||||
if _, err := t.HTTPURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -206,7 +211,7 @@ func NormalizeAvailabilityTarget(target AvailabilityTarget) AvailabilityTarget {
|
|||
target.ID = strings.TrimSpace(target.ID)
|
||||
target.Name = strings.TrimSpace(target.Name)
|
||||
target.TargetKind = AvailabilityTargetKind(strings.ToLower(strings.TrimSpace(string(target.TargetKind))))
|
||||
target.Protocol = AvailabilityProbeProtocol(strings.ToLower(strings.TrimSpace(string(target.Protocol))))
|
||||
target.Protocol = normalizeAvailabilityProbeProtocol(target.Protocol)
|
||||
if target.Protocol == AvailabilityProbeHTTP || target.Protocol == AvailabilityProbeHTTPS {
|
||||
target.Address = strings.TrimSpace(target.Address)
|
||||
} else {
|
||||
|
|
@ -218,6 +223,14 @@ func NormalizeAvailabilityTarget(target AvailabilityTarget) AvailabilityTarget {
|
|||
return target
|
||||
}
|
||||
|
||||
func normalizeAvailabilityProbeProtocol(protocol AvailabilityProbeProtocol) AvailabilityProbeProtocol {
|
||||
normalized := AvailabilityProbeProtocol(strings.ToLower(strings.TrimSpace(string(protocol))))
|
||||
if normalized == availabilityProbePingAlias {
|
||||
return AvailabilityProbeICMP
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeAvailabilityAddress(raw string) string {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,21 @@ func TestNormalizeAvailabilityTargetReducesICMPAddressToHost(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAvailabilityTargetAcceptsPingAlias(t *testing.T) {
|
||||
target := NormalizeAvailabilityTarget(AvailabilityTarget{
|
||||
Address: " https://device.local:8443/status ",
|
||||
Protocol: AvailabilityProbeProtocol(" Ping "),
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
if target.Protocol != AvailabilityProbeICMP {
|
||||
t.Fatalf("Protocol = %q, want %q", target.Protocol, AvailabilityProbeICMP)
|
||||
}
|
||||
if target.Address != "device.local" {
|
||||
t.Fatalf("Address = %q, want device.local", target.Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityTargetProbeAddressUsesHTTPHostname(t *testing.T) {
|
||||
target := NormalizeAvailabilityTarget(AvailabilityTarget{
|
||||
Address: "http://solar-inverter.lab.local/status",
|
||||
|
|
@ -86,6 +101,19 @@ func TestAvailabilityTargetValidateRejectsTCPWithoutPort(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityTargetValidateAcceptsPingAlias(t *testing.T) {
|
||||
target := AvailabilityTarget{
|
||||
TargetKind: AvailabilityTargetService,
|
||||
Address: "device.local",
|
||||
Protocol: AvailabilityProbeProtocol("ping"),
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
if err := target.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailabilityTargetValidateRejectsUnsupportedTargetKind(t *testing.T) {
|
||||
target := NormalizeAvailabilityTarget(AvailabilityTarget{
|
||||
TargetKind: AvailabilityTargetKind("database"),
|
||||
|
|
|
|||
|
|
@ -2916,7 +2916,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
{
|
||||
"heading": "## Shared Boundaries",
|
||||
"path": "internal/api/access_control_handlers.go",
|
||||
"line": 1088,
|
||||
"line": 1093,
|
||||
"heading_line": 137,
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue