Files
oott/nix/modules/oott-service.nix
rzuastiandClaude Opus 4.8 be2a8fce58 Make [notifications.pushover] optional for non-pushover methods
The pushover config section is now only required when notifications.method
is "pushover". Validation at startup rejects the missing-section case so a
misconfiguration fails fast instead of erroring on every notification.

Updates the sample TOML, README and nix module for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:45:02 -04:00

178 lines
6.3 KiB
Nix

{
config,
pkgs,
lib ? pkgs.lib,
...
}:
with lib; let
cfg = config.services.oott;
in {
# Service options
options.services.oott = rec {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to run the oott service
'';
};
database.path = mkOption {
type = types.path;
description = "Full path to store the database at";
default = "/var/lib/oott.db";
};
networking.interface = mkOption {
type = types.nullOr types.str;
description = "Network interface to use for scans. If not set, the first non-loopback connected interface is used automatically.";
default = null;
};
log.level = mkOption {
type = types.str;
description = "Log level for the oott service";
default = "warn";
};
arp_scanner.enabled = mkOption {
type = types.bool;
description = "Whether to run the ARP scanner.";
default = true;
};
arp_scanner.wait_between_scans = mkOption {
type = types.str;
description = "Wait time between scans. This does not include the scan time.";
default = "30m";
};
arp_scanner.sender_timeout = mkOption {
type = types.str;
description = "If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute).";
default = "1m";
};
arp_scanner.scan_duration = mkOption {
type = types.str;
description = "How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network).";
default = "10m";
};
mdns_scanner.enabled = mkOption {
type = types.bool;
description = "Whether to run the mDNS/Bonjour scanner.";
default = true;
};
mdns_scanner.probe_timeout = mkOption {
type = types.str;
description = "When an mDNS-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address.";
default = "2s";
};
ssdp_scanner.enabled = mkOption {
type = types.bool;
description = "Whether to run the SSDP/UPnP scanner.";
default = true;
};
ssdp_scanner.probe_timeout = mkOption {
type = types.str;
description = "When an SSDP/UPnP-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address.";
default = "2s";
};
dhcp_scanner.enabled = mkOption {
type = types.bool;
description = "Whether to run the DHCP scanner.";
default = true;
};
snmp_scanner.enabled = mkOption {
type = types.bool;
description = "Whether to run the SNMP scanner. Off by default; requires a target and community to be set.";
default = false;
};
snmp_scanner.target = mkOption {
type = types.str;
description = "SNMP agent to poll, as host:port (e.g. the gateway: 192.168.1.1:161). The scanner reads its ARP table over SNMPv2c.";
default = "";
};
snmp_scanner.community = mkOption {
type = types.str;
description = "SNMPv2c read-only community string. Use a read-only community and avoid committing real secrets.";
default = "";
};
snmp_scanner.wait_between_scans = mkOption {
type = types.str;
description = "Wait time between SNMP polls. Keep it well under the agent's ARP cache timeout so active devices aren't missed.";
default = "10m";
};
snmp_scanner.timeout = mkOption {
type = types.str;
description = "Per-poll SNMP request timeout.";
default = "5s";
};
notifications.method = mkOption {
type = types.str;
description = "For now just pushover, you can set this to none to avoid sending notifications (it will just log).";
default = "pushover";
};
notifications.notify_when_not_seen_for = mkOption {
type = types.str;
description = "Send a notification if a device comes back online after not being seen for this timeframe.";
default = "1w";
};
notifications.pushover = mkOption {
type = types.nullOr (types.submodule {
options = {
token = mkOption {
type = types.str;
description = "Your pushover token goes here, just copy&paste from their website after creating the app.";
default = "";
};
user_key = mkOption {
type = types.str;
description = "User key goes here, this is the account wide code for pushover.";
default = "";
};
};
});
description = "Pushover credentials. Only required when notifications.method is \"pushover\"; leave it null (the default) for any other method.";
default = null;
};
retention.window = mkOption {
type = types.str;
description = "How long to retain device events and notifications. Records older than this are purged daily. Accepts duration strings (e.g. 90d, 1y, 6m).";
default = "365d";
};
device_events.deduplication_window = mkOption {
type = types.str;
description = "If the same scanner sees the same device (same MAC and IP) again within this window, only one device event is recorded. Keeps the events table from filling with near-identical rows. Accepts duration strings (e.g. 30s, 1m, 5m).";
default = "1m";
};
web_server.ip_address = mkOption {
type = types.str;
description = "IP address to bind the web server (API and web UI) to. Use 0.0.0.0 to bind to all interfaces.";
default = "0.0.0.0";
};
web_server.port = mkOption {
type = types.port;
description = "Port the web server listens on.";
default = 3000;
};
web_server.api_key = mkOption {
type = types.str;
description = "API key required to access the system's API. Change this from the default empty value.";
default = "";
};
};
# Service implementation
config = mkIf cfg.enable {
environment.systemPackages = [pkgs.oott];
systemd.services.oott = {
description = "oott - network device scanner";
wantedBy = ["multi-user.target"];
serviceConfig = {
ExecStart = "${pkgs.oott}/bin/oott --config ${
builtins.toFile "oott.json"
(generators.toJSON {} cfg)
}";
ProtectHome = "read-only";
Restart = "on-failure";
Type = "exec";
};
};
};
}