feat(checks): vm-status probe + matrix cert dial-by-name
VMs declared monitoring [ping], but many block ICMP and lack a guest agent (haos), so ping was the wrong probe — a powered-on VM reported "down". Add a vm-status check: `qm status <pve_id>` on the VM's Proxmox host, which tests "powered on" without needing the VM's network at all. vm type monitoring is now [vm-status]. matrix.hubris.network is a public hostname (federation) resolving to netbird-vps, not served by the lab Caddy — so its cert-expiry check's dial=caddy IP failed. Drop the dial for matrix; it dials by name (DNS -> public) like wget already proved works.
This commit is contained in:
@@ -32,6 +32,7 @@ const (
|
|||||||
KindCapacity = "capacity"
|
KindCapacity = "capacity"
|
||||||
KindBackup = "backup-freshness"
|
KindBackup = "backup-freshness"
|
||||||
KindCertExpiry = "cert-expiry"
|
KindCertExpiry = "cert-expiry"
|
||||||
|
KindVMStatus = "vm-status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultBackupMaxAge is how long a backup target may go without a new
|
// defaultBackupMaxAge is how long a backup target may go without a new
|
||||||
@@ -295,6 +296,19 @@ func buildKind(kind string, t Target, attrs map[string]any, host, user string, p
|
|||||||
config: config,
|
config: config,
|
||||||
interval: 3600,
|
interval: 3600,
|
||||||
}}, ""
|
}}, ""
|
||||||
|
|
||||||
|
case KindVMStatus:
|
||||||
|
// "Is the VM powered on" via `qm status` on its Proxmox host — the
|
||||||
|
// right reachability probe for a VM, since many block ICMP and lack a
|
||||||
|
// guest agent. checkVMStatus re-reads pve_id + host at runtime.
|
||||||
|
if _, ok := attrs["pve_id"]; !ok {
|
||||||
|
return nil, "no pve_id to run qm status"
|
||||||
|
}
|
||||||
|
return []checkDef{{
|
||||||
|
kind: "vm-status",
|
||||||
|
config: map[string]any{},
|
||||||
|
interval: 60,
|
||||||
|
}}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, "no builder for this kind yet"
|
return nil, "no builder for this kind yet"
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ func executeCheck(ctx context.Context, pool *db.Pool, cd sqlcgen.ListEnabledChec
|
|||||||
return checkDisk(ctx, cd)
|
return checkDisk(ctx, cd)
|
||||||
case "cert-expiry":
|
case "cert-expiry":
|
||||||
return checkCertExpiry(ctx, cd)
|
return checkCertExpiry(ctx, cd)
|
||||||
|
case "vm-status":
|
||||||
|
return checkVMStatus(ctx, pool, cd)
|
||||||
case "ping":
|
case "ping":
|
||||||
return checkPing(ctx, cd)
|
return checkPing(ctx, cd)
|
||||||
case "ssh-script":
|
case "ssh-script":
|
||||||
@@ -613,6 +615,53 @@ func checkCertExpiry(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) ch
|
|||||||
return checkResult{health: "healthy", metrics: metrics}
|
return checkResult{health: "healthy", metrics: metrics}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkVMStatus reports whether a VM is powered on, via `qm status <pve_id>`
|
||||||
|
// run on its Proxmox host. This is the right reachability probe for a VM that
|
||||||
|
// blocks ICMP (haos) and has no guest agent: it doesn't need the VM's network
|
||||||
|
// at all — "status: running" means the VM is up. The command runs on the host
|
||||||
|
// (not inside the VM), so it uses the host's address with identity wrap.
|
||||||
|
func checkVMStatus(ctx context.Context, pool *db.Pool, cd sqlcgen.ListEnabledCheckDefsRow) checkResult {
|
||||||
|
if cd.TargetID == nil {
|
||||||
|
return checkResult{health: "unknown", evidence: "vm-status needs a target VM"}
|
||||||
|
}
|
||||||
|
var pveID, hostAttr string
|
||||||
|
if err := pool.QueryRow(ctx,
|
||||||
|
"SELECT attributes->>'pve_id', COALESCE(attributes->>'host','') FROM entities WHERE id = $1",
|
||||||
|
*cd.TargetID).Scan(&pveID, &hostAttr); err != nil || pveID == "" {
|
||||||
|
return checkResult{health: "unknown", signalKind: "vm-status",
|
||||||
|
evidence: fmt.Sprintf("vm %s has no pve_id", cd.EntitySlug)}
|
||||||
|
}
|
||||||
|
hostSlug := remote.ResolveProxmoxHostSlug(ctx, pool, *cd.TargetID, hostAttr)
|
||||||
|
addr, user, err := remote.ResolveHost(ctx, pool, hostSlug, sshUser)
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{health: "down", signalKind: "vm-status",
|
||||||
|
evidence: fmt.Sprintf("resolve proxmox host for %s: %v", cd.EntitySlug, err), err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Duration(cd.TimeoutS) * time.Second
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 15 * time.Second
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
out, err := sshExec(ctx, addr, "22", user, "qm status "+pveID, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{health: "down", signalKind: "vm-status",
|
||||||
|
evidence: fmt.Sprintf("qm status %s on %s: %v", pveID, addr, err), err: err}
|
||||||
|
}
|
||||||
|
// `qm status <id>` prints "status: running" (or stopped/paused).
|
||||||
|
if strings.Contains(string(out), "status: running") {
|
||||||
|
return checkResult{health: "healthy"}
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimSpace(string(out))
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "(no output)"
|
||||||
|
}
|
||||||
|
return checkResult{health: "down", signalKind: "vm-status",
|
||||||
|
evidence: fmt.Sprintf("%s not running: %s", cd.EntitySlug, trimmed)}
|
||||||
|
}
|
||||||
|
|
||||||
// checkPing performs an ICMP ping check using the system ping command.
|
// checkPing performs an ICMP ping check using the system ping command.
|
||||||
func checkPing(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) checkResult {
|
func checkPing(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) checkResult {
|
||||||
cfg := struct {
|
cfg := struct {
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ entities:
|
|||||||
- {slug: "cert:media.hubris.network", type: certificate, name: media.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:media.hubris.network", type: certificate, name: media.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
- {slug: "cert:cloud.hubris.network", type: certificate, name: cloud.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:cloud.hubris.network", type: certificate, name: cloud.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
- {slug: "cert:paperless.hubris.network", type: certificate, name: paperless.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:paperless.hubris.network", type: certificate, name: paperless.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
- {slug: "cert:matrix.hubris.network", type: certificate, name: matrix.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:matrix.hubris.network", type: certificate, name: matrix.hubris.network}
|
||||||
- {slug: "cert:photos.hubris.network", type: certificate, name: photos.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:photos.hubris.network", type: certificate, name: photos.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
- {slug: "cert:artifacto.hubris.network", type: certificate, name: artifacto.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:artifacto.hubris.network", type: certificate, name: artifacto.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
- {slug: "cert:trmnl.hubris.network", type: certificate, name: trmnl.hubris.network, attributes: {dial: "192.168.8.175"}}
|
- {slug: "cert:trmnl.hubris.network", type: certificate, name: trmnl.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
|||||||
@@ -272,7 +272,9 @@ entity_types:
|
|||||||
layer: infrastructure
|
layer: infrastructure
|
||||||
lifecycle: infrastructure
|
lifecycle: infrastructure
|
||||||
description: Virtual machine.
|
description: Virtual machine.
|
||||||
monitoring: [ping] # no guest agent assumed; reachability only
|
monitoring: [vm-status] # `qm status` from the host: powered-on liveness
|
||||||
|
# that works even when the VM blocks ICMP and has
|
||||||
|
# no guest agent (haos). ping is unreliable for VMs.
|
||||||
attributes:
|
attributes:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
Reference in New Issue
Block a user