From 3519bdcec4f03f408461c9d5db9ef21d467c7633 Mon Sep 17 00:00:00 2001
From: clerie <git@clerie.de>
Date: Sun, 18 Jun 2023 21:51:21 +0200
Subject: [PATCH] Add NixOS modules

---
 flake.nix                |  21 ++++++
 nix/modules/dhcp.nix     | 141 +++++++++++++++++++++++++++++++++++
 nix/modules/fieldpoc.nix | 156 +++++++++++++++++++++++++++++++++++++++
 nix/modules/yate.nix     |  56 ++++++++++++++
 4 files changed, 374 insertions(+)
 create mode 100644 nix/modules/dhcp.nix
 create mode 100644 nix/modules/fieldpoc.nix
 create mode 100644 nix/modules/yate.nix

diff --git a/flake.nix b/flake.nix
index 13b63d7..2ccc30c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -100,7 +100,28 @@
         pythonImportsCheck = [ "diffsync" ];
       };
 
+      yate = pkgs.yate.overrideAttrs (old: {
+        configureFlags = [ "--with-libpq=${pkgs.postgresql.withPackages (ps: [ ])}" ];
+      });
+    };
 
+    nixosModules = {
+      fieldpoc = { ... }: {
+        imports = [
+          ./nix/modules/dhcp.nix
+          ./nix/modules/fieldpoc.nix
+          ./nix/modules/yate.nix
+        ];
+
+        nixpkgs.overlays = [
+          (_: _: {
+            inherit (self.packages."x86_64-linux")
+              fieldpoc
+              yate;
+          })
+        ];
+      };
+      default = self.nixosModules.fieldpoc;
     };
 
     hydraJobs = {
diff --git a/nix/modules/dhcp.nix b/nix/modules/dhcp.nix
new file mode 100644
index 0000000..22c7674
--- /dev/null
+++ b/nix/modules/dhcp.nix
@@ -0,0 +1,141 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fieldpoc.dhcp;
+in {
+  options.services.fieldpoc.dhcp = {
+    enable = mkEnableOption "fieldpoc-dhcp";
+    interface = mkOption {
+      type = types.str;
+    };
+    subnet = mkOption {
+      type = types.str;
+    };
+    pool = mkOption {
+      type = types.str;
+    };
+    router = mkOption {
+      type = types.str;
+    };
+    dnsServers = mkOption {
+      type = types.str;
+    };
+    omm = mkOption {
+      type = types.str;
+    };
+    reservations = mkOption {
+      type = with types; listOf (submodule {
+        options = {
+          name = mkOption {
+            type = types.str;
+          };
+          macAddress = mkOption {
+            type = types.str;
+          };
+          ipAddress = mkOption {
+            type = types.str;
+          };
+        };
+      });
+      default = [];
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.kea.dhcp4 = {
+      enable = true;
+      settings = {
+        interfaces-config = {
+          interfaces = [ cfg.interface ];
+        };
+        option-def = [
+          {
+            space = "dhcp4";
+            name = "vendor-encapsulated-options";
+            code = 43;
+            type = "empty";
+            encapsulate = "sipdect";
+          }
+          {
+            space = "sipdect";
+            name = "ommip1";
+            code = 10;
+            type = "ipv4-address";
+          }
+          {
+            space = "sipdect";
+            name = "ommip2";
+            code = 19;
+            type = "ipv4-address";
+          }
+          {
+            space = "sipdect";
+            name = "syslogip";
+            code = 14;
+            type = "ipv4-address";
+          }
+          {
+            space = "sipdect";
+            name = "syslogport";
+            code = 15;
+            type = "int16";
+          }
+          {
+            space = "dhcp4";
+            name = "magic_str";
+            code = 224;
+            type = "string";
+          }
+        ];
+
+        subnet4 = [
+          {
+            subnet = cfg.subnet;
+            pools = [
+              {
+                pool = cfg.pool;
+              }
+            ];
+            option-data = [
+              {
+                name = "routers";
+                data = cfg.router;
+              }
+              {
+                name = "domain-name-servers";
+                data = cfg.dnsServers;
+              }
+              {
+                name = "vendor-encapsulated-options";
+              }
+              {
+                space = "sipdect";
+                name = "ommip1";
+                data = cfg.omm;
+              }
+              {
+                name = "magic_str";
+                data = "OpenMobilitySIP-DECT";
+              }
+            ];
+
+            reservations = map (r: {
+              hostname = r.name;
+              hw-address = r.macAddress;
+              ip-address = r.ipAddress;
+              option-data = [
+                {
+                  name = "host-name";
+                  data = r.name;
+                }
+              ];
+            }) cfg.reservations;
+          }
+        ];
+      };
+    };
+  };
+}
+
diff --git a/nix/modules/fieldpoc.nix b/nix/modules/fieldpoc.nix
new file mode 100644
index 0000000..58cfbd4
--- /dev/null
+++ b/nix/modules/fieldpoc.nix
@@ -0,0 +1,156 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.fieldpoc;
+in {
+  options = {
+    services.fieldpoc = {
+      enable = mkEnableOption "fieldpoc";
+      ommIp = mkOption {
+        type = types.str;
+      };
+      ommUser = mkOption {
+        type = types.str;
+      };
+      ommPasswordPath = mkOption {
+        type = types.path;
+      };
+      sipsecretPath = mkOption {
+        type = types.path;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [
+      fieldpoc
+    ];
+
+    systemd.services.fieldpoc = {
+      description = "Fieldpoc daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "yate.service" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.fieldpoc}/bin/fieldpoc -c /run/fieldpoc/config.json --debug";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "fieldpoc";
+        Group = "fieldpoc";
+        RuntimeDirectory = "fieldpoc";
+        RuntimeDirectoryMode = "0755";
+        ConfigurationDirectory = "fieldpoc";
+        StateDirectory = "fieldpoc";
+        StateDirectoryMode = "0700";
+      };
+
+      preStart = let
+        cfgFile = pkgs.writeText "config.json" (lib.generators.toJSON { } {
+          extensions = {
+            file = "/var/lib/fieldpoc/extensions.json";
+          };
+          controller = {
+            host = "127.0.0.1";
+            port = 9437;
+          };
+          dect = {
+            host = cfg.ommIp;
+            username = cfg.ommUser;
+            password = "!!OMMPASSWORD!!";
+            sipsecret = "!!SIPSECRET!!";
+          };
+          yate = {
+            host = "127.0.0.1";
+            port = 5039;
+          };
+          database = {
+            hostname = "127.0.0.1";
+            username = "fieldpoc";
+            password = "fieldpoc";
+            database = "fieldpoc";
+          };
+        });
+      in ''
+        ${pkgs.gnused}/bin/sed -e "s/!!OMMPASSWORD!!/$(cat ${cfg.ommPasswordPath})/g" -e "s/!!SIPSECRET!!/$(cat ${cfg.sipsecretPath})/g" ${cfgFile} > /run/fieldpoc/config.json
+        if [ ! -f "/var/lib/fieldpoc/extensions.json" ]; then
+          echo '{"extensions": {}}' > /var/lib/fieldpoc/extensions.json
+        fi
+      '';
+    };
+
+    users.users.fieldpoc = {
+      isSystemUser = true;
+      group = "fieldpoc";
+    };
+
+    users.groups.fieldpoc = { };
+
+    services.postgresql = {
+      enable = true;
+      initialScript = pkgs.writeText "backend-initScript" ''
+        CREATE ROLE fieldpoc WITH LOGIN PASSWORD 'fieldpoc' CREATEDB;
+        CREATE DATABASE fieldpoc;
+        GRANT ALL PRIVILEGES ON DATABASE fieldpoc TO fieldpoc;
+      '';
+    };
+
+    services.yate = {
+      enable = true;
+      config = {
+        extmodule = {
+          "listener ywsd" = {
+            type = "tcp";
+            addr = "127.0.0.1";
+            port = "5039";
+          };
+        };
+        cdrbuild = {
+          parameters = {
+            X-Eventphone-Id = "false";
+          };
+        };
+        pgsqldb = {
+          default = {
+            host = "localhost";
+            port = "5432";
+            database = "fieldpoc";
+            user = "fieldpoc";
+            password = "fieldpoc";
+          };
+        };
+        register = {
+          general = {
+            expires = 30;
+            "user.auth" = "yes";
+            "user.register" = "yes";
+            "user.unregister" = "yes";
+            "engine.timer" = "yes";
+            "call.cdr" = "yes";
+            "linetracker" = "yes";
+          };
+          default = {
+            priority = 30;
+            account = "default";
+          };
+          "user.auth" = {
+            query = "SELECT password FROM users WHERE username='\${username}' AND password IS NOT NULL AND password<>'' AND type='user' LIMIT 1;";
+            result = "password";
+          };
+          "user.register".query = "INSERT INTO registrations (username, location, oconnection_id, expires) VALUES ('\${username}', '\${data}', '\${oconnection_id}', NOW() + INTERVAL '\${expires} s') ON CONFLICT ON CONSTRAINT uniq_registrations DO UPDATE SET expires = NOW() + INTERVAL '\${expires} s'";
+          "user.unregister".query = "DELETE FROM registrations WHERE (username = '\${username}' AND location = '\${data}' AND oconnection_id = '\${connection_id}') OR ('\${username}' = '' AND '\${data}' = '' AND oconnection_id = '\${connection_id}')";
+          "engine.timer".query = "DELETE FROM registrations WHERE expires<=CURRENT_TIMESTAMP;";
+          "call.cdr".critial = "no";
+          "linetracker" = {
+            critical = "yes";
+            initquery = "UPDATE users SET inuse=0 WHERE inuse is not NULL;DELETE from active_calls;";
+            cdr_initialize = "UPDATE users SET inuse=inuse+1 WHERE username='\${external}';INSERT INTO active_calls SELECT username, x_eventphone_id FROM (SELECT '\${external}' as username, '\${X-Eventphone-Id}' as x_eventphone_id, '\${direction}' as direction) as active_call WHERE x_eventphone_id != '' AND x_eventphone_id IS NOT NULL and direction = 'outgoing';";
+            cdr_finalize = "UPDATE users SET inuse=(CASE WHEN inuse>0 THEN inuse-1 ELSE 0 END) WHERE username='\${external}';DELETE FROM active_calls WHERE username = '\${external}' AND  x_eventphone_id = '\${X-Eventphone-Id}' AND '\${direction}' = 'outgoing';";
+          };
+        };
+      };
+    };
+  };
+}
+
diff --git a/nix/modules/yate.nix b/nix/modules/yate.nix
new file mode 100644
index 0000000..3084a6b
--- /dev/null
+++ b/nix/modules/yate.nix
@@ -0,0 +1,56 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.yate;
+in {
+  options = {
+    services.yate = {
+      enable = mkEnableOption "yate";
+      config = mkOption {
+        type = with types; attrsOf anything;
+        default = { };
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.etc = mapAttrs' (name: config: nameValuePair "yate/${name}.conf" { text = (if (isAttrs config) then generators.toINI {} config else config); }) cfg.config;
+
+    systemd.services.yate = {
+      description = "YATE Telephony Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" "postgresql.service" ];
+
+      environment = { PWLIB_ASSERT_ACTION = "C"; };
+
+      serviceConfig = {
+        Type = "forking";
+        ExecStart =
+          "${pkgs.yate}/bin/yate -d -p /run/yate/yate.pid -c /etc/yate -F -s -vvv -DF -r -l /var/lib/yate/yate.log";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "yate";
+        Group = "yate";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        RuntimeDirectory = "yate";
+        RuntimeDirectoryMode = "0755";
+        ConfigurationDirectory = "yate";
+        StateDirectory = "yate";
+        StateDirectoryMode = "0700";
+        PIDFile = "/run/yate/yate.pid";
+        TimeoutSec = 30;
+      };
+
+      reloadTriggers = map (name: config.environment.etc."yate/${name}.conf".source) (attrNames cfg.config);
+    };
+
+    users.users.yate = {
+      isSystemUser = true;
+      group = "yate";
+    };
+
+    users.groups.yate = { };
+  };
+}
+