feat: add NixOS module and proper Nix packaging
- nix/package.nix: two-phase build with fixed-output derivation for deps - nix/nixos-module.nix: systemd service with systant.enable and systant.configFile - flake.nix: expose nixosModules.default and overlays.default Usage in NixOS config: systant.enable = true; systant.configFile = ./systant.toml; When deps change, update hash: nix build .#systant 2>&1 | grep 'got:' Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.systant;
|
||||
settingsFormat = pkgs.formats.toml { };
|
||||
in
|
||||
{
|
||||
options.systant = {
|
||||
enable = lib.mkEnableOption "systant system monitoring agent";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.systant;
|
||||
defaultText = lib.literalExpression "pkgs.systant";
|
||||
description = "The systant package to use.";
|
||||
};
|
||||
|
||||
configFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to the systant configuration file (TOML).
|
||||
If set, this takes precedence over the settings option.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = { };
|
||||
description = ''
|
||||
Configuration for systant in Nix attribute set form.
|
||||
Will be converted to TOML. Ignored if configFile is set.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
mqtt = {
|
||||
broker = "mqtt://localhost:1883";
|
||||
topicPrefix = "systant";
|
||||
};
|
||||
entities = {
|
||||
cpu_usage = {
|
||||
type = "sensor";
|
||||
state_command = "awk '/^cpu / {u=$2+$4; t=$2+$4+$5; print int(u*100/t)}' /proc/stat";
|
||||
unit = "%";
|
||||
icon = "mdi:cpu-64-bit";
|
||||
name = "CPU Usage";
|
||||
};
|
||||
};
|
||||
homeassistant = {
|
||||
discovery = true;
|
||||
discoveryPrefix = "homeassistant";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "systant";
|
||||
description = "User account under which systant runs.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "systant";
|
||||
description = "Group under which systant runs.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Create systant user/group if using defaults
|
||||
users.users.${cfg.user} = lib.mkIf (cfg.user == "systant") {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
description = "Systant service user";
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = lib.mkIf (cfg.group == "systant") { };
|
||||
|
||||
# Generate config file from settings if configFile not provided
|
||||
environment.etc."systant/config.toml" = lib.mkIf (cfg.configFile == null && cfg.settings != { }) {
|
||||
source = settingsFormat.generate "systant-config.toml" cfg.settings;
|
||||
};
|
||||
|
||||
systemd.services.systant = {
|
||||
description = "Systant system monitoring agent";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart =
|
||||
let
|
||||
configPath =
|
||||
if cfg.configFile != null
|
||||
then cfg.configFile
|
||||
else "/etc/systant/config.toml";
|
||||
in
|
||||
"${cfg.package}/bin/systant run --config ${configPath}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
|
||||
# Allow reading system metrics
|
||||
ReadOnlyPaths = [
|
||||
"/proc"
|
||||
"/sys"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
stdenv,
|
||||
bun,
|
||||
cacert,
|
||||
}:
|
||||
|
||||
let
|
||||
# Fixed-output derivation to fetch npm dependencies
|
||||
# Update the hash when bun.lock changes by running:
|
||||
# nix build .#systant 2>&1 | grep 'got:'
|
||||
deps = stdenvNoCC.mkDerivation {
|
||||
pname = "systant-deps";
|
||||
version = "0.1.0";
|
||||
|
||||
src = lib.fileset.toSource {
|
||||
root = ./..;
|
||||
fileset = lib.fileset.unions [
|
||||
./../package.json
|
||||
./../bun.lock
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ bun cacert ];
|
||||
|
||||
buildPhase = ''
|
||||
export HOME=$TMPDIR
|
||||
bun install --frozen-lockfile
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
cp -r node_modules $out
|
||||
'';
|
||||
|
||||
outputHashMode = "recursive";
|
||||
outputHashAlgo = "sha256";
|
||||
# To update: nix build .#systant 2>&1 | grep 'got:'
|
||||
outputHash = "sha256-hQ1ZzOFOHHeaAtyfCXxX6jpqB7poFLwavgMW8yMwaHw=";
|
||||
};
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "systant";
|
||||
version = "0.1.0";
|
||||
|
||||
src = lib.fileset.toSource {
|
||||
root = ./..;
|
||||
fileset = lib.fileset.unions [
|
||||
./../index.ts
|
||||
./../src
|
||||
./../package.json
|
||||
./../tsconfig.json
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ bun ];
|
||||
|
||||
buildPhase = ''
|
||||
export HOME=$TMPDIR
|
||||
cp -r ${deps} node_modules
|
||||
chmod -R u+w node_modules
|
||||
bun build index.ts --compile --outfile systant
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp systant $out/bin/systant
|
||||
'';
|
||||
|
||||
# Bun's compiled binaries don't like being stripped
|
||||
dontStrip = true;
|
||||
|
||||
meta = with lib; {
|
||||
description = "System monitoring agent with MQTT and Home Assistant integration";
|
||||
homepage = "https://git.ryanpandya.com/ryan/systant";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user