Initial commit: IT Nexus Web-App
This commit is contained in:
659
nexus-scanner/internal/db/db.go
Normal file
659
nexus-scanner/internal/db/db.go
Normal file
@@ -0,0 +1,659 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Store wraps the SQLite database.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Open opens (or creates) the SQLite database at path and runs migrations.
|
||||
func Open(path string) (*Store, error) {
|
||||
conn, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite at %q: %w", path, err)
|
||||
}
|
||||
|
||||
// SQLite works best with a single writer connection.
|
||||
conn.SetMaxOpenConns(1)
|
||||
|
||||
// Recommended pragmas for reliability and performance.
|
||||
for _, pragma := range []string{
|
||||
"PRAGMA journal_mode=WAL",
|
||||
"PRAGMA busy_timeout=5000",
|
||||
"PRAGMA foreign_keys=ON",
|
||||
} {
|
||||
if _, err := conn.Exec(pragma); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("pragma %q: %w", pragma, err)
|
||||
}
|
||||
}
|
||||
|
||||
s := &Store{db: conn}
|
||||
if err := s.migrate(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying database connection.
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func (s *Store) migrate() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS host_ports (
|
||||
ip TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
scanned_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (ip, port)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS snmp_targets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
community TEXT NOT NULL DEFAULT 'public',
|
||||
version TEXT NOT NULL DEFAULT 'v2c',
|
||||
type TEXT NOT NULL DEFAULT 'generic',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS snmp_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_id INTEGER NOT NULL,
|
||||
oid_name TEXT NOT NULL,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
scanned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(target_id, oid_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS module_states (
|
||||
name TEXT PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS monitor_checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS monitor_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
check_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
latency_ms INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_results_check
|
||||
ON monitor_results(check_id, checked_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ad_computers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cn TEXT NOT NULL UNIQUE,
|
||||
dn TEXT NOT NULL DEFAULT '',
|
||||
os TEXT NOT NULL DEFAULT '',
|
||||
os_version TEXT NOT NULL DEFAULT '',
|
||||
owner TEXT NOT NULL DEFAULT '',
|
||||
department TEXT NOT NULL DEFAULT '',
|
||||
last_logon DATETIME,
|
||||
ad_created DATETIME,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hosts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip TEXT NOT NULL UNIQUE,
|
||||
mac TEXT NOT NULL,
|
||||
vendor TEXT NOT NULL DEFAULT '',
|
||||
hostname TEXT NOT NULL DEFAULT '',
|
||||
site TEXT NOT NULL,
|
||||
first_seen DATETIME NOT NULL,
|
||||
last_seen DATETIME NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'online'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scan_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
module TEXT NOT NULL,
|
||||
started_at DATETIME NOT NULL,
|
||||
ended_at DATETIME,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
error TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// Host represents a discovered network device.
|
||||
type Host struct {
|
||||
ID int64
|
||||
IP string
|
||||
MAC string
|
||||
Vendor string
|
||||
Hostname string
|
||||
Site string
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
Status string
|
||||
}
|
||||
|
||||
// UpsertHost inserts a new host or updates MAC, vendor and last_seen if the
|
||||
// IP already exists. first_seen is never overwritten on update.
|
||||
func (s *Store) UpsertHost(h Host) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO hosts (ip, mac, vendor, hostname, site, first_seen, last_seen, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'online')
|
||||
ON CONFLICT(ip) DO UPDATE SET
|
||||
mac = excluded.mac,
|
||||
vendor = CASE WHEN excluded.vendor != '' THEN excluded.vendor ELSE vendor END,
|
||||
hostname = CASE WHEN excluded.hostname != '' THEN excluded.hostname ELSE hostname END,
|
||||
last_seen = excluded.last_seen,
|
||||
status = 'online'
|
||||
`, h.IP, h.MAC, h.Vendor, h.Hostname, h.Site, h.FirstSeen.UTC(), h.LastSeen.UTC())
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert host %s: %w", h.IP, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListHosts returns all hosts ordered by last_seen desc.
|
||||
// Pass an empty site to return all sites.
|
||||
func (s *Store) ListHosts(site string) ([]Host, error) {
|
||||
query := `
|
||||
SELECT id, ip, mac, vendor, hostname, site, first_seen, last_seen, status
|
||||
FROM hosts`
|
||||
var args []any
|
||||
if site != "" {
|
||||
query += ` WHERE site = ?`
|
||||
args = append(args, site)
|
||||
}
|
||||
query += ` ORDER BY last_seen DESC`
|
||||
|
||||
rows, err := s.db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list hosts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var hosts []Host
|
||||
for rows.Next() {
|
||||
var h Host
|
||||
if err := rows.Scan(
|
||||
&h.ID, &h.IP, &h.MAC, &h.Vendor, &h.Hostname,
|
||||
&h.Site, &h.FirstSeen, &h.LastSeen, &h.Status,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan host row: %w", err)
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
return hosts, rows.Err()
|
||||
}
|
||||
|
||||
// CountHosts returns the total number of hosts in the database.
|
||||
func (s *Store) CountHosts() (int, error) {
|
||||
var n int
|
||||
if err := s.db.QueryRow(`SELECT COUNT(*) FROM hosts`).Scan(&n); err != nil {
|
||||
return 0, fmt.Errorf("count hosts: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// BeginScan records the start of a module scan run and returns the run ID.
|
||||
func (s *Store) BeginScan(module string) (int64, error) {
|
||||
res, err := s.db.Exec(`
|
||||
INSERT INTO scan_runs (module, started_at) VALUES (?, ?)
|
||||
`, module, time.Now().UTC())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("begin scan: %w", err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// EndScan marks a scan run as completed (status ok or error).
|
||||
func (s *Store) EndScan(id int64, scanErr error) error {
|
||||
status, errMsg := "ok", ""
|
||||
if scanErr != nil {
|
||||
status, errMsg = "error", scanErr.Error()
|
||||
}
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE scan_runs SET ended_at = ?, status = ?, error = ? WHERE id = ?
|
||||
`, time.Now().UTC(), status, errMsg, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("end scan %d: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ADComputer represents a computer object from Active Directory.
|
||||
type ADComputer struct {
|
||||
ID int64
|
||||
CN string
|
||||
DN string
|
||||
OS string
|
||||
OSVersion string
|
||||
Owner string
|
||||
Department string
|
||||
LastLogon time.Time
|
||||
ADCreated time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// UpsertADComputer inserts or updates an AD computer record by CN.
|
||||
func (s *Store) UpsertADComputer(c ADComputer) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO ad_computers (cn, dn, os, os_version, owner, department, last_logon, ad_created, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(cn) DO UPDATE SET
|
||||
dn = excluded.dn,
|
||||
os = excluded.os,
|
||||
os_version = excluded.os_version,
|
||||
owner = excluded.owner,
|
||||
department = excluded.department,
|
||||
last_logon = excluded.last_logon,
|
||||
updated_at = excluded.updated_at
|
||||
`, c.CN, c.DN, c.OS, c.OSVersion, c.Owner, c.Department,
|
||||
nullTime(c.LastLogon), nullTime(c.ADCreated), time.Now().UTC())
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert ad computer %s: %w", c.CN, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListADComputers returns all known AD computers.
|
||||
func (s *Store) ListADComputers() ([]ADComputer, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, cn, dn, os, os_version, owner, department, last_logon, ad_created, updated_at
|
||||
FROM ad_computers ORDER BY cn
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list ad computers: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []ADComputer
|
||||
for rows.Next() {
|
||||
var c ADComputer
|
||||
var lastLogon, adCreated *time.Time
|
||||
if err := rows.Scan(&c.ID, &c.CN, &c.DN, &c.OS, &c.OSVersion,
|
||||
&c.Owner, &c.Department, &lastLogon, &adCreated, &c.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lastLogon != nil {
|
||||
c.LastLogon = *lastLogon
|
||||
}
|
||||
if adCreated != nil {
|
||||
c.ADCreated = *adCreated
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateHostnameFromAD sets the hostname of a host matching the given IP.
|
||||
func (s *Store) UpdateHostnameFromAD(hostname string) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE hosts SET hostname = ? WHERE hostname = '' AND ip IN (
|
||||
SELECT ip FROM hosts WHERE LOWER(hostname) = LOWER(?)
|
||||
)
|
||||
`, hostname, hostname)
|
||||
return err
|
||||
}
|
||||
|
||||
func nullTime(t time.Time) any {
|
||||
if t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return t.UTC()
|
||||
}
|
||||
|
||||
// LastScanTime returns when the given module last ran successfully.
|
||||
func (s *Store) LastScanTime(module string) (time.Time, error) {
|
||||
var t time.Time
|
||||
err := s.db.QueryRow(`
|
||||
SELECT ended_at FROM scan_runs
|
||||
WHERE module = ? AND status = 'ok'
|
||||
ORDER BY ended_at DESC LIMIT 1
|
||||
`, module).Scan(&t)
|
||||
if err == sql.ErrNoRows {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return t, err
|
||||
}
|
||||
|
||||
/* ── Site Monitoring ──────────────────────────────────────────────── */
|
||||
|
||||
// MonitorCheck is a configured availability check (ping/http/tcp).
|
||||
type MonitorCheck struct {
|
||||
ID int64
|
||||
Name string
|
||||
Type string // ping | http | tcp
|
||||
Target string
|
||||
Enabled bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// MonitorResult is a single check execution result.
|
||||
type MonitorResult struct {
|
||||
ID int64
|
||||
CheckID int64
|
||||
Status string // online | offline
|
||||
LatencyMS int
|
||||
Error string
|
||||
CheckedAt time.Time
|
||||
}
|
||||
|
||||
// ListMonitorChecks returns all configured checks ordered by creation time.
|
||||
func (s *Store) ListMonitorChecks() ([]MonitorCheck, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, name, type, target, enabled, created_at
|
||||
FROM monitor_checks ORDER BY id
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list monitor checks: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []MonitorCheck
|
||||
for rows.Next() {
|
||||
var c MonitorCheck
|
||||
var ena int
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.Target, &ena, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Enabled = ena == 1
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// AddMonitorCheck inserts a new check definition.
|
||||
func (s *Store) AddMonitorCheck(name, typ, target string) (int64, error) {
|
||||
res, err := s.db.Exec(`
|
||||
INSERT INTO monitor_checks (name, type, target) VALUES (?, ?, ?)
|
||||
`, name, typ, target)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("add monitor check: %w", err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// DeleteMonitorCheck removes a check and all its results.
|
||||
func (s *Store) DeleteMonitorCheck(id int64) error {
|
||||
if _, err := s.db.Exec(`DELETE FROM monitor_results WHERE check_id = ?`, id); err != nil {
|
||||
return fmt.Errorf("delete monitor results: %w", err)
|
||||
}
|
||||
if _, err := s.db.Exec(`DELETE FROM monitor_checks WHERE id = ?`, id); err != nil {
|
||||
return fmt.Errorf("delete monitor check: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertMonitorResult stores one check execution result.
|
||||
func (s *Store) InsertMonitorResult(r MonitorResult) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO monitor_results (check_id, status, latency_ms, error, checked_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, r.CheckID, r.Status, r.LatencyMS, r.Error, r.CheckedAt.UTC())
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert monitor result: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LatestMonitorResults returns the most recent result for each check_id.
|
||||
func (s *Store) LatestMonitorResults() (map[int64]MonitorResult, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT r.id, r.check_id, r.status, r.latency_ms, r.error, r.checked_at
|
||||
FROM monitor_results r
|
||||
INNER JOIN (
|
||||
SELECT check_id, MAX(id) as max_id FROM monitor_results GROUP BY check_id
|
||||
) latest ON r.id = latest.max_id
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("latest monitor results: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[int64]MonitorResult)
|
||||
for rows.Next() {
|
||||
var r MonitorResult
|
||||
if err := rows.Scan(&r.ID, &r.CheckID, &r.Status, &r.LatencyMS, &r.Error, &r.CheckedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[r.CheckID] = r
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// MonitorResultHistory returns the last limit results for a given check.
|
||||
func (s *Store) MonitorResultHistory(checkID int64, limit int) ([]MonitorResult, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, check_id, status, latency_ms, error, checked_at
|
||||
FROM monitor_results WHERE check_id = ?
|
||||
ORDER BY checked_at DESC LIMIT ?
|
||||
`, checkID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("monitor history: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []MonitorResult
|
||||
for rows.Next() {
|
||||
var r MonitorResult
|
||||
if err := rows.Scan(&r.ID, &r.CheckID, &r.Status, &r.LatencyMS, &r.Error, &r.CheckedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
// Reverse so oldest-first for sparkline rendering.
|
||||
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
|
||||
out[i], out[j] = out[j], out[i]
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
/* ── Module States ────────────────────────────────────────────────── */
|
||||
|
||||
// SetModuleEnabled stores the enabled state for a module.
|
||||
func (s *Store) SetModuleEnabled(name string, enabled bool) error {
|
||||
v := 0
|
||||
if enabled {
|
||||
v = 1
|
||||
}
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO module_states (name, enabled) VALUES (?,?)
|
||||
ON CONFLICT(name) DO UPDATE SET enabled=excluded.enabled
|
||||
`, name, v)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetModuleEnabled returns whether a module is enabled (default true if not set).
|
||||
func (s *Store) GetModuleEnabled(name string) bool {
|
||||
var v int
|
||||
err := s.db.QueryRow(`SELECT enabled FROM module_states WHERE name=?`, name).Scan(&v)
|
||||
if err != nil {
|
||||
return true // default: enabled
|
||||
}
|
||||
return v == 1
|
||||
}
|
||||
|
||||
/* ── Port Scan ────────────────────────────────────────────────────── */
|
||||
|
||||
// UpsertHostPorts replaces all open ports for an IP.
|
||||
func (s *Store) UpsertHostPorts(ip string, ports []int) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(`DELETE FROM host_ports WHERE ip = ?`, ip); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
for _, p := range ports {
|
||||
if _, err := tx.Exec(`INSERT INTO host_ports (ip, port, scanned_at) VALUES (?,?,?)`, ip, p, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetHostPorts returns all open ports for an IP.
|
||||
func (s *Store) GetHostPorts(ip string) ([]int, error) {
|
||||
rows, err := s.db.Query(`SELECT port FROM host_ports WHERE ip = ? ORDER BY port`, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ports []int
|
||||
for rows.Next() {
|
||||
var p int
|
||||
rows.Scan(&p)
|
||||
ports = append(ports, p)
|
||||
}
|
||||
return ports, rows.Err()
|
||||
}
|
||||
|
||||
// ListAllHostPorts returns a map of IP → open ports.
|
||||
func (s *Store) ListAllHostPorts() (map[string][]int, error) {
|
||||
rows, err := s.db.Query(`SELECT ip, port FROM host_ports ORDER BY ip, port`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[string][]int)
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
var port int
|
||||
rows.Scan(&ip, &port)
|
||||
out[ip] = append(out[ip], port)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// MarkStaleHostsOffline marks hosts as offline if last_seen is older than threshold.
|
||||
func (s *Store) MarkStaleHostsOffline(olderThan time.Duration) (int, error) {
|
||||
threshold := time.Now().UTC().Add(-olderThan)
|
||||
res, err := s.db.Exec(`UPDATE hosts SET status='offline' WHERE status='online' AND last_seen < ?`, threshold)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("mark stale offline: %w", err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
/* ── SNMP ─────────────────────────────────────────────────────────── */
|
||||
|
||||
// SNMPTarget is a configured SNMP polling target.
|
||||
type SNMPTarget struct {
|
||||
ID int64
|
||||
Name string
|
||||
IP string
|
||||
Community string
|
||||
Version string
|
||||
Type string // generic | printer | switch
|
||||
Enabled bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// SNMPResult is the latest value for one OID on one target.
|
||||
type SNMPResult struct {
|
||||
ID int64
|
||||
TargetID int64
|
||||
OIDName string
|
||||
Value string
|
||||
ScannedAt time.Time
|
||||
}
|
||||
|
||||
// ListSNMPTargets returns all SNMP targets.
|
||||
func (s *Store) ListSNMPTargets() ([]SNMPTarget, error) {
|
||||
rows, err := s.db.Query(`SELECT id,name,ip,community,version,type,enabled,created_at FROM snmp_targets ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SNMPTarget
|
||||
for rows.Next() {
|
||||
var t SNMPTarget
|
||||
var ena int
|
||||
rows.Scan(&t.ID, &t.Name, &t.IP, &t.Community, &t.Version, &t.Type, &ena, &t.CreatedAt)
|
||||
t.Enabled = ena == 1
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// AddSNMPTarget inserts a new SNMP target.
|
||||
func (s *Store) AddSNMPTarget(name, ip, community, version, typ string) (int64, error) {
|
||||
res, err := s.db.Exec(`INSERT INTO snmp_targets (name,ip,community,version,type) VALUES (?,?,?,?,?)`,
|
||||
name, ip, community, version, typ)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// DeleteSNMPTarget removes a target and its results.
|
||||
func (s *Store) DeleteSNMPTarget(id int64) error {
|
||||
s.db.Exec(`DELETE FROM snmp_results WHERE target_id = ?`, id)
|
||||
_, err := s.db.Exec(`DELETE FROM snmp_targets WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpsertSNMPResult inserts or updates one OID result.
|
||||
func (s *Store) UpsertSNMPResult(r SNMPResult) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO snmp_results (target_id, oid_name, value, scanned_at)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(target_id, oid_name) DO UPDATE SET value=excluded.value, scanned_at=excluded.scanned_at
|
||||
`, r.TargetID, r.OIDName, r.Value, r.ScannedAt.UTC())
|
||||
return err
|
||||
}
|
||||
|
||||
// LatestSNMPResults returns all latest OID values keyed by target_id.
|
||||
func (s *Store) LatestSNMPResults() (map[int64]map[string]string, error) {
|
||||
rows, err := s.db.Query(`SELECT target_id, oid_name, value FROM snmp_results ORDER BY target_id, oid_name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[int64]map[string]string)
|
||||
for rows.Next() {
|
||||
var tid int64
|
||||
var k, v string
|
||||
rows.Scan(&tid, &k, &v)
|
||||
if out[tid] == nil {
|
||||
out[tid] = make(map[string]string)
|
||||
}
|
||||
out[tid][k] = v
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// CleanupMonitorResults deletes old results, keeping the last keepPerCheck per check.
|
||||
func (s *Store) CleanupMonitorResults(keepPerCheck int) error {
|
||||
_, err := s.db.Exec(`
|
||||
DELETE FROM monitor_results WHERE id NOT IN (
|
||||
SELECT id FROM (
|
||||
SELECT id, ROW_NUMBER() OVER (
|
||||
PARTITION BY check_id ORDER BY checked_at DESC
|
||||
) AS rn FROM monitor_results
|
||||
) WHERE rn <= ?
|
||||
)
|
||||
`, keepPerCheck)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user