660 lines
19 KiB
Go
660 lines
19 KiB
Go
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
|
|
}
|