mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Add distinctive-port fast-path for un-named workloads
The name fast-path identifies workloads named after their service, but a workload with a generic name (ct101, db1) still fell through to the model — which, with a slow reasoning model configured, times out rather than just being slow. Many such workloads are still identifiable instantly by a DISTINCTIVE listening port. Add a Ports field to the identity table (only single-service ports: 8123 HA, 32400 Plex, 5432 postgres, 6379 redis, 1883/8883 mosquitto, 3306 mariadb, 8086 influxdb, 8096 jellyfin, 9090 prometheus, 6052 esphome) and a second fast-path inferSurfaceIdentityFromPorts that runs after the name path and before the model. Ambiguous ports (80, 443, 8080, 3000, 5000) are deliberately excluded — a bad port guess is worse than asking the model. Parser validated against real ss -tlnp output from LXC 101 (extracts 8123, ignores ephemeral ports / PIDs / docker-proxy noise → home-assistant). Unit tests cover distinctive-port matches and ambiguous-only non-matches.
This commit is contained in:
parent
ddd9aaddba
commit
ac1628f9b4
3 changed files with 109 additions and 16 deletions
|
|
@ -1793,6 +1793,18 @@ func (s *Service) DiscoverResource(ctx context.Context, req DiscoveryRequest) (*
|
|||
CurrentStep: "Identified from resource name (fast path)",
|
||||
PercentComplete: 90,
|
||||
})
|
||||
} else if identity, evidence, ok := inferSurfaceIdentityFromPorts(analysisReq.CommandOutputs["listening_ports"]); ok {
|
||||
// Second fast path: an un-named workload often exposes a distinctive
|
||||
// listening port that identifies it (8123→HA, 32400→Plex). Skip the
|
||||
// model here too — a distinctive port is a high-confidence signal, and
|
||||
// the configured model may be slow enough to time out otherwise.
|
||||
result = surfaceIdentityResponse(identity, evidence)
|
||||
s.broadcastProgress(&DiscoveryProgress{
|
||||
ResourceID: resourceID,
|
||||
Status: DiscoveryStatusRunning,
|
||||
CurrentStep: "Identified from listening port (fast path)",
|
||||
PercentComplete: 90,
|
||||
})
|
||||
} else if metadataOnly {
|
||||
result = metadataOnlyDiscoveryAbstention()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package servicediscovery
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -10,29 +12,37 @@ type knownServiceIdentity struct {
|
|||
ServiceName string
|
||||
Category ServiceCategory
|
||||
Aliases []string
|
||||
Confidence float64
|
||||
// Ports lists DISTINCTIVE listening ports that belong to essentially one
|
||||
// service (8123→HA, 32400→Plex). The port fast-path uses these to identify
|
||||
// an un-named workload without the model. Ambiguous ports (80, 443, 8080,
|
||||
// 3000, 5000) are deliberately omitted — a bad port guess is worse than
|
||||
// letting the model decide.
|
||||
Ports []int
|
||||
Confidence float64
|
||||
}
|
||||
|
||||
// knownServiceIdentities drives both the deterministic surface fast-path
|
||||
// (inferSurfaceIdentity, name-based, runs before the model) and the post-model
|
||||
// identity improver (applyKnownServiceIdentity). Common homelab services are
|
||||
// usually named after themselves, so the resource name alone identifies them
|
||||
// instantly — no model call. Aliases are matched against the normalized name.
|
||||
// knownServiceIdentities drives the deterministic surface fast-paths
|
||||
// (inferSurfaceIdentity by name and inferSurfaceIdentityFromPorts by distinctive
|
||||
// listening port, both run before the model) and the post-model identity
|
||||
// improver (applyKnownServiceIdentity). Common homelab services are usually
|
||||
// named after themselves and/or listen on a distinctive port, so they identify
|
||||
// instantly — no model call. Aliases match the normalized name; Ports match
|
||||
// exact listening ports.
|
||||
var knownServiceIdentities = []knownServiceIdentity{
|
||||
{ServiceType: "home-assistant", ServiceName: "Home Assistant", Category: CategoryHomeAuto, Aliases: []string{"home assistant", "homeassistant", "hassio", "hass"}, Confidence: 0.9},
|
||||
{ServiceType: "esphome", ServiceName: "ESPHome", Category: CategoryHomeAuto, Aliases: []string{"esphome", "esp-home", "esp home"}, Confidence: 0.85},
|
||||
{ServiceType: "home-assistant", ServiceName: "Home Assistant", Category: CategoryHomeAuto, Aliases: []string{"home assistant", "homeassistant", "hassio", "hass"}, Ports: []int{8123}, Confidence: 0.9},
|
||||
{ServiceType: "esphome", ServiceName: "ESPHome", Category: CategoryHomeAuto, Aliases: []string{"esphome", "esp-home", "esp home"}, Ports: []int{6052}, Confidence: 0.85},
|
||||
{ServiceType: "zigbee2mqtt", ServiceName: "Zigbee2MQTT", Category: CategoryHomeAuto, Aliases: []string{"zigbee2mqtt", "zigbee 2 mqtt"}, Confidence: 0.9},
|
||||
{ServiceType: "frigate", ServiceName: "Frigate NVR", Category: CategoryNVR, Aliases: []string{"frigate"}, Confidence: 0.9},
|
||||
{ServiceType: "mosquitto", ServiceName: "Mosquitto MQTT", Category: CategoryNetwork, Aliases: []string{"mosquitto", "mqtt"}, Confidence: 0.85},
|
||||
{ServiceType: "postgresql", ServiceName: "PostgreSQL", Category: CategoryDatabase, Aliases: []string{"postgresql", "postgres"}, Confidence: 0.9},
|
||||
{ServiceType: "mariadb", ServiceName: "MariaDB", Category: CategoryDatabase, Aliases: []string{"mariadb", "mysqld", "mysql"}, Confidence: 0.85},
|
||||
{ServiceType: "redis", ServiceName: "Redis", Category: CategoryCache, Aliases: []string{"redis", "redis server"}, Confidence: 0.9},
|
||||
{ServiceType: "influxdb", ServiceName: "InfluxDB", Category: CategoryDatabase, Aliases: []string{"influxdb", "influx"}, Confidence: 0.85},
|
||||
{ServiceType: "plex", ServiceName: "Plex Media Server", Category: CategoryMedia, Aliases: []string{"plex media server", "plexmediaserver", "plex"}, Confidence: 0.9},
|
||||
{ServiceType: "jellyfin", ServiceName: "Jellyfin", Category: CategoryMedia, Aliases: []string{"jellyfin"}, Confidence: 0.9},
|
||||
{ServiceType: "mosquitto", ServiceName: "Mosquitto MQTT", Category: CategoryNetwork, Aliases: []string{"mosquitto", "mqtt"}, Ports: []int{1883, 8883}, Confidence: 0.85},
|
||||
{ServiceType: "postgresql", ServiceName: "PostgreSQL", Category: CategoryDatabase, Aliases: []string{"postgresql", "postgres"}, Ports: []int{5432}, Confidence: 0.9},
|
||||
{ServiceType: "mariadb", ServiceName: "MariaDB", Category: CategoryDatabase, Aliases: []string{"mariadb", "mysqld", "mysql"}, Ports: []int{3306}, Confidence: 0.85},
|
||||
{ServiceType: "redis", ServiceName: "Redis", Category: CategoryCache, Aliases: []string{"redis", "redis server"}, Ports: []int{6379}, Confidence: 0.9},
|
||||
{ServiceType: "influxdb", ServiceName: "InfluxDB", Category: CategoryDatabase, Aliases: []string{"influxdb", "influx"}, Ports: []int{8086}, Confidence: 0.85},
|
||||
{ServiceType: "plex", ServiceName: "Plex Media Server", Category: CategoryMedia, Aliases: []string{"plex media server", "plexmediaserver", "plex"}, Ports: []int{32400}, Confidence: 0.9},
|
||||
{ServiceType: "jellyfin", ServiceName: "Jellyfin", Category: CategoryMedia, Aliases: []string{"jellyfin"}, Ports: []int{8096}, Confidence: 0.9},
|
||||
{ServiceType: "nginx", ServiceName: "Nginx", Category: CategoryWebServer, Aliases: []string{"nginx"}, Confidence: 0.8},
|
||||
{ServiceType: "grafana", ServiceName: "Grafana", Category: CategoryMonitoring, Aliases: []string{"grafana"}, Confidence: 0.9},
|
||||
{ServiceType: "prometheus", ServiceName: "Prometheus", Category: CategoryMonitoring, Aliases: []string{"prometheus"}, Confidence: 0.9},
|
||||
{ServiceType: "prometheus", ServiceName: "Prometheus", Category: CategoryMonitoring, Aliases: []string{"prometheus"}, Ports: []int{9090}, Confidence: 0.9},
|
||||
{ServiceType: "uptime-kuma", ServiceName: "Uptime Kuma", Category: CategoryMonitoring, Aliases: []string{"uptime kuma", "uptimekuma"}, Confidence: 0.9},
|
||||
{ServiceType: "pihole", ServiceName: "Pi-hole", Category: CategoryNetwork, Aliases: []string{"pi hole", "pihole"}, Confidence: 0.9},
|
||||
{ServiceType: "adguard", ServiceName: "AdGuard Home", Category: CategoryNetwork, Aliases: []string{"adguard home", "adguardhome", "adguard"}, Confidence: 0.9},
|
||||
|
|
@ -182,6 +192,47 @@ func inferSurfaceIdentity(req DiscoveryRequest, metadata map[string]any) (knownS
|
|||
return knownServiceIdentity{}, "", false
|
||||
}
|
||||
|
||||
// listeningPortPattern extracts port numbers that appear after a colon in
|
||||
// ss/netstat output (the local-address column, e.g. "0.0.0.0:8123"). Only
|
||||
// distinctive ports are matched downstream, so any stray match is harmless.
|
||||
var listeningPortPattern = regexp.MustCompile(`:(\d{2,5})\b`)
|
||||
|
||||
// parseListeningPorts returns the set of port numbers present in ss/netstat
|
||||
// output.
|
||||
func parseListeningPorts(output string) map[int]bool {
|
||||
ports := map[int]bool{}
|
||||
for _, match := range listeningPortPattern.FindAllStringSubmatch(output, -1) {
|
||||
if p, err := strconv.Atoi(match[1]); err == nil {
|
||||
ports[p] = true
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
// inferSurfaceIdentityFromPorts is the second deterministic fast-path: when the
|
||||
// name doesn't identify the workload, a distinctive listening port often does
|
||||
// (8123→Home Assistant, 32400→Plex). Only ports that belong to essentially one
|
||||
// service are in the table, so a match is high-confidence and the model can be
|
||||
// skipped — important when the configured model is slow and would otherwise time
|
||||
// out on an un-named workload.
|
||||
func inferSurfaceIdentityFromPorts(listeningPortsOutput string) (knownServiceIdentity, string, bool) {
|
||||
if strings.TrimSpace(listeningPortsOutput) == "" {
|
||||
return knownServiceIdentity{}, "", false
|
||||
}
|
||||
ports := parseListeningPorts(listeningPortsOutput)
|
||||
if len(ports) == 0 {
|
||||
return knownServiceIdentity{}, "", false
|
||||
}
|
||||
for _, identity := range knownServiceIdentities {
|
||||
for _, p := range identity.Ports {
|
||||
if ports[p] {
|
||||
return identity, fmt.Sprintf("listening port %d", p), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return knownServiceIdentity{}, "", false
|
||||
}
|
||||
|
||||
// surfaceIdentityResponse builds a no-model discovery result from a fast-path
|
||||
// match. Identity only: config/data/log paths are intentionally left empty —
|
||||
// the Assistant knows standard service layouts and fetches specifics on demand,
|
||||
|
|
|
|||
|
|
@ -92,3 +92,33 @@ func TestWithNestedDockerAccess(t *testing.T) {
|
|||
t.Fatalf("expected layered docker exec path, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInferSurfaceIdentityFromPorts(t *testing.T) {
|
||||
haSS := "State Recv-Q Send-Q Local-Address:Port Peer:Port Process\n" +
|
||||
"LISTEN 0 4096 0.0.0.0:8123 0.0.0.0:* users:((\"python3\",pid=1234,fd=10))\n" +
|
||||
"LISTEN 0 128 127.0.0.1:22 0.0.0.0:*"
|
||||
cases := []struct {
|
||||
name string
|
||||
output string
|
||||
wantType string
|
||||
wantOK bool
|
||||
}{
|
||||
{"HA by distinctive port 8123", haSS, "home-assistant", true},
|
||||
{"postgres by 5432", "LISTEN 0 244 0.0.0.0:5432 0.0.0.0:*", "postgresql", true},
|
||||
{"plex by 32400", "LISTEN 0 128 *:32400 *:*", "plex", true},
|
||||
{"ambiguous ports only do not match", "LISTEN 0 128 0.0.0.0:80\nLISTEN 0 128 0.0.0.0:443\nLISTEN 0 128 0.0.0.0:22", "", false},
|
||||
{"empty output", "", "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
id, _, ok := inferSurfaceIdentityFromPorts(tc.output)
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("inferSurfaceIdentityFromPorts ok=%v, want %v", ok, tc.wantOK)
|
||||
}
|
||||
if ok && id.ServiceType != tc.wantType {
|
||||
t.Fatalf("inferSurfaceIdentityFromPorts type=%q, want %q", id.ServiceType, tc.wantType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue