Commit 49305054 authored by Paul Meyer's avatar Paul Meyer
Browse files

nixos/speedtest-tracker: init module

parent 1d9a649e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -1755,6 +1755,7 @@
  ./services/web-apps/snipe-it.nix
  ./services/web-apps/snips-sh.nix
  ./services/web-apps/sogo.nix
  ./services/web-apps/speedtest-tracker.nix
  ./services/web-apps/sshwifty.nix
  ./services/web-apps/stash.nix
  ./services/web-apps/stirling-pdf.nix
+429 −0
Original line number Diff line number Diff line
{
  pkgs,
  config,
  lib,
  ...
}:

let
  cfg = config.services.speedtest-tracker;

  user = cfg.user;
  group = cfg.group;

  defaultUser = "speedtest-tracker";
  defaultGroup = "speedtest-tracker";

  artisan = "${cfg.package}/artisan";

  env-file-values = lib.mapAttrs' (n: v: {
    name = lib.removeSuffix "_FILE" n;
    value = v;
  }) (lib.filterAttrs (n: v: v != null && lib.match ".+_FILE" n != null) cfg.settings);

  env-nonfile-values = lib.filterAttrs (n: v: lib.match ".+_FILE" n == null) cfg.settings;

  speedtest-tracker-maintenance = pkgs.writeShellScript "speedtest-tracker-maintenance.sh" ''
    set -a
    ${lib.toShellVars env-nonfile-values}
    ${lib.concatLines (lib.mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)}
    set +a
    ${lib.optionalString (
      cfg.settings.DB_CONNECTION == "sqlite"
    ) "touch ${cfg.dataDir}/storage/database/database.sqlite"}
    rm -f ${cfg.dataDir}/cache/*.php
    ${artisan} package:discover
    ${artisan} migrate --force
    ${artisan} optimize:clear
    ${artisan} view:cache
    ${artisan} config:cache
  '';

  speedtest-tracker-env-script =
    command:
    pkgs.writeShellScript "speedtest-tracker-${builtins.replaceStrings [ ":" " " ] [ "-" "-" ] command}.sh" ''
      set -a
      ${lib.toShellVars env-nonfile-values}
      ${lib.concatLines (lib.mapAttrsToList (n: v: "${n}=\"$(< ${v})\"") env-file-values)}
      set +a
      exec ${artisan} ${command}
    '';

  commonServiceConfig = {
    Type = "oneshot";
    User = user;
    Group = group;
    StateDirectory = "speedtest-tracker";
    ReadWritePaths = [ cfg.dataDir ];
    WorkingDirectory = cfg.package;
    PrivateTmp = true;
    PrivateDevices = true;
    CapabilityBoundingSet = "";
    AmbientCapabilities = "";
    ProtectSystem = "strict";
    ProtectKernelTunables = true;
    ProtectKernelModules = true;
    ProtectControlGroups = true;
    ProtectClock = true;
    ProtectHostname = true;
    ProtectHome = "tmpfs";
    ProtectKernelLogs = true;
    ProtectProc = "invisible";
    ProcSubset = "pid";
    PrivateNetwork = false;
    RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
    SystemCallArchitectures = "native";
    SystemCallFilter = [
      "@system-service @resources"
      "~@obsolete @privileged"
    ];
    RestrictSUIDSGID = true;
    RemoveIPC = true;
    NoNewPrivileges = true;
    RestrictRealtime = true;
    RestrictNamespaces = true;
    LockPersonality = true;
    PrivateUsers = true;
  };

in
{
  options.services.speedtest-tracker = {

    enable = lib.mkEnableOption "Speedtest Tracker: A self-hosted internet performance tracking application";

    package =
      lib.mkPackageOption pkgs "speedtest-tracker" { }
      // lib.mkOption {
        apply =
          speedtest-tracker:
          speedtest-tracker.override (prev: {
            dataDir = cfg.dataDir;
          });
      };

    user = lib.mkOption {
      type = lib.types.str;
      default = defaultUser;
      description = "User account under which Speedtest Tracker runs.";
    };

    group = lib.mkOption {
      type = lib.types.str;
      default = if cfg.enableNginx then "nginx" else defaultGroup;
      defaultText = "If `services.speedtest-tracker.enableNginx` is true then `nginx` else ${defaultGroup}";
      description = ''
        Group under which Speedtest Tracker runs. It is best to set this to the group
        of whatever webserver is being used as the frontend.
      '';
    };

    dataDir = lib.mkOption {
      type = lib.types.path;
      default = "/var/lib/speedtest-tracker";
      description = ''
        The place where Speedtest Tracker stores its state.
      '';
    };

    enableNginx = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = ''
        Whether to enable nginx or not. If enabled, an nginx virtual host will
        be created for access to Speedtest Tracker. If not enabled, then you may use
        `''${config.services.speedtest-tracker.package}` as your document root in
        whichever webserver you wish to setup.
      '';
    };

    virtualHost = lib.mkOption {
      type = lib.types.str;
      default = "localhost";
      description = ''
        The hostname at which you wish Speedtest Tracker to be served. If you have
        enabled nginx using `services.speedtest-tracker.enableNginx` then this will
        be used.
      '';
    };

    poolConfig = lib.mkOption {
      type = lib.types.attrsOf (
        lib.types.oneOf [
          lib.types.str
          lib.types.int
          lib.types.bool
        ]
      );
      default = { };
      defaultText = ''
        {
          "pm" = "dynamic";
          "pm.max_children" = 32;
          "pm.start_servers" = 2;
          "pm.min_spare_servers" = 2;
          "pm.max_spare_servers" = 4;
          "pm.max_requests" = 500;
        }
      '';
      description = ''
        Options for the Speedtest Tracker PHP pool. See the documentation on `php-fpm.conf`
        for details on configuration directives.
      '';
    };

    settings = lib.mkOption {
      default = { };
      description = ''
        Options for Speedtest Tracker configuration. Refer to
        <https://github.com/alexjustesen/speedtest-tracker> for
        details on supported values. All `_FILE` values are supported:
        append `_FILE` to the setting name to provide a path to a file
        containing the secret value.
      '';
      example = lib.literalExpression ''
        {
          APP_KEY_FILE = "/var/secrets/speedtest-tracker-app-key.txt";
          DB_CONNECTION = "mysql";
          DB_HOST = "db";
          DB_PORT = 3306;
          DB_DATABASE = "speedtest-tracker";
          DB_USERNAME = "speedtest-tracker";
          DB_PASSWORD_FILE = "/var/secrets/speedtest-tracker-mysql-password.txt";
        }
      '';
      type = lib.types.submodule {
        freeformType = lib.types.attrsOf (
          lib.types.oneOf [
            lib.types.str
            lib.types.int
            lib.types.bool
          ]
        );
        options = {
          APP_KEY_FILE = lib.mkOption {
            type = lib.types.path;
            description = ''
              The path to your appkey. The file should contain a 32 character
              random app key. This may be set using `echo "base64:$(head -c 32
              /dev/urandom | base64)" > /path/to/key-file`.
            '';
          };
          APP_URL = lib.mkOption {
            type = lib.types.str;
            default =
              if cfg.virtualHost == "localhost" then
                "http://${cfg.virtualHost}"
              else
                "https://${cfg.virtualHost}";
            defaultText = ''
              http(s)://''${config.services.speedtest-tracker.virtualHost}
            '';
            description = ''
              The APP_URL used by Speedtest Tracker internally. Please make sure this
              URL matches the external URL of your installation. It is used to
              validate specific requests and to generate URLs in notifications.
            '';
          };
          DB_CONNECTION = lib.mkOption {
            type = lib.types.enum [
              "sqlite"
              "mysql"
              "mariadb"
              "pgsql"
            ];
            default = "sqlite";
            example = "pgsql";
            description = ''
              The type of database you wish to use. Can be one of "sqlite",
              "mysql", "mariadb" or "pgsql".
            '';
          };
          DB_HOST = lib.mkOption {
            type = lib.types.str;
            default = "localhost";
            description = ''
              The IP or hostname which hosts your database.
            '';
          };
          DB_PORT = lib.mkOption {
            type = lib.types.nullOr lib.types.int;
            default =
              if cfg.settings.DB_CONNECTION == "pgsql" then
                5432
              else if cfg.settings.DB_CONNECTION == "mysql" || cfg.settings.DB_CONNECTION == "mariadb" then
                3306
              else
                null;
            defaultText = ''
              `null` if DB_CONNECTION is "sqlite", `3306` if "mysql" or "mariadb", `5432` if "pgsql"
            '';
            description = ''
              The port your database is listening at. sqlite does not require
              this value to be filled.
            '';
          };
          DB_DATABASE = lib.mkOption {
            type = lib.types.str;
            default =
              if cfg.settings.DB_CONNECTION == "sqlite" then
                "${cfg.dataDir}/storage/database/database.sqlite"
              else
                "speedtest-tracker";
            defaultText = ''
              "''${config.services.speedtest-tracker.dataDir}/storage/database/database.sqlite" if DB_CONNECTION is "sqlite", "speedtest-tracker" otherwise
            '';
            description = ''
              The name of the database, or path to the sqlite file.
            '';
          };
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {

    services.phpfpm.pools.speedtest-tracker = {
      inherit user group;
      phpPackage = cfg.package.phpPackage;
      phpOptions = ''
        log_errors = on
      '';
      settings = {
        "listen.mode" = lib.mkDefault "0660";
        "listen.owner" = lib.mkDefault user;
        "listen.group" = lib.mkDefault group;
        "pm" = lib.mkDefault "dynamic";
        "pm.max_children" = lib.mkDefault 32;
        "pm.start_servers" = lib.mkDefault 2;
        "pm.min_spare_servers" = lib.mkDefault 2;
        "pm.max_spare_servers" = lib.mkDefault 4;
        "pm.max_requests" = lib.mkDefault 500;
      }
      // cfg.poolConfig;
    };

    systemd.services.speedtest-tracker-setup = {
      after = [
        "postgresql.target"
        "mysql.service"
      ];
      requiredBy = [ "phpfpm-speedtest-tracker.service" ];
      before = [ "phpfpm-speedtest-tracker.service" ];
      serviceConfig = {
        ExecStart = speedtest-tracker-maintenance;
        RemainAfterExit = true;
      }
      // commonServiceConfig;
      unitConfig.JoinsNamespaceOf = "phpfpm-speedtest-tracker.service";
      restartTriggers = [ cfg.package ];
      partOf = [ "phpfpm-speedtest-tracker.service" ];
    };

    systemd.services.speedtest-tracker-scheduler = {
      after = [ "speedtest-tracker-setup.service" ];
      wants = [ "speedtest-tracker-setup.service" ];
      description = "Speedtest Tracker scheduler";
      path = [ pkgs.ookla-speedtest ];
      serviceConfig = {
        ExecStart = speedtest-tracker-env-script "schedule:run";
      }
      // commonServiceConfig;
    };

    systemd.timers.speedtest-tracker-scheduler = {
      description = "Speedtest Tracker scheduler timer";
      timerConfig = {
        OnCalendar = "minutely";
        Persistent = true;
      };
      wantedBy = [ "timers.target" ];
      restartTriggers = [ cfg.package ];
    };

    systemd.services.speedtest-tracker-queue-worker = {
      after = [ "speedtest-tracker-setup.service" ];
      wants = [ "speedtest-tracker-setup.service" ];
      wantedBy = [ "multi-user.target" ];
      description = "Speedtest Tracker queue worker";
      path = [ pkgs.ookla-speedtest ];
      serviceConfig = commonServiceConfig // {
        Type = "simple";
        Restart = "always";
        ExecStart = speedtest-tracker-env-script "queue:work --sleep=3 --tries=3";
      };
    };

    services.nginx = lib.mkIf cfg.enableNginx {
      enable = true;
      recommendedTlsSettings = lib.mkDefault true;
      recommendedOptimisation = lib.mkDefault true;
      recommendedGzipSettings = lib.mkDefault true;
      virtualHosts.${cfg.virtualHost} = {
        root = "${cfg.package}/public";
        locations = {
          "/" = {
            tryFiles = "$uri $uri/ /index.php?$query_string";
            index = "index.php";
            extraConfig = ''
              sendfile off;
            '';
          };
          "~ \\.php$" = {
            extraConfig = ''
              include ${config.services.nginx.package}/conf/fastcgi_params;
              fastcgi_param SCRIPT_FILENAME $request_filename;
              fastcgi_param modHeadersAvailable true;
              fastcgi_pass unix:${config.services.phpfpm.pools.speedtest-tracker.socket};
            '';
          };
          "~ \\.(js|css|gif|png|ico|jpg|jpeg)$" = {
            extraConfig = "expires 365d;";
          };
        };
      };
    };

    systemd.tmpfiles.settings."10-speedtest-tracker" =
      lib.genAttrs
        [
          "${cfg.dataDir}/storage"
          "${cfg.dataDir}/storage/app"
          "${cfg.dataDir}/storage/database"
          "${cfg.dataDir}/storage/framework"
          "${cfg.dataDir}/storage/framework/cache"
          "${cfg.dataDir}/storage/framework/sessions"
          "${cfg.dataDir}/storage/framework/views"
          "${cfg.dataDir}/storage/logs"
          "${cfg.dataDir}/cache"
        ]
        (n: {
          d = {
            inherit group;
            mode = "0700";
            inherit user;
          };
        })
      // {
        "${cfg.dataDir}".d = {
          inherit group;
          mode = "0710";
          inherit user;
        };
      };

    users = {
      users = lib.mkIf (user == defaultUser) {
        ${defaultUser} = {
          inherit group;
          isSystemUser = true;
          home = cfg.dataDir;
        };
      };
      groups = lib.mkIf (group == defaultGroup) { ${defaultGroup} = { }; };
    };
  };

  meta.maintainers = pkgs.speedtest-tracker.meta.maintainers;
}
+1 −0
Original line number Diff line number Diff line
@@ -1484,6 +1484,7 @@ in
  sonic-server = runTest ./sonic-server.nix;
  spacecookie = runTest ./spacecookie.nix;
  spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark { };
  speedtest-tracker = runTest ./speedtest-tracker.nix;
  spiped = runTest ./spiped.nix;
  sqlite3-to-mysql = runTest ./sqlite3-to-mysql.nix;
  squid = runTest ./squid.nix;
+33 −0
Original line number Diff line number Diff line
{ lib, pkgs, ... }:

let
  app-key = "base64:VGVzdFRlc3RUZXN0VGVzdFRlc3RUZXN0VGVzdFRlc3Q=";
in
{
  name = "speedtest-tracker";
  meta = {
    maintainers = pkgs.speedtest-tracker.meta.maintainers;
    platforms = lib.platforms.linux;
  };

  nodes.sqlite = {
    environment.etc."speedtest-tracker-appkey".text = app-key;
    services.speedtest-tracker = {
      enable = true;
      enableNginx = true;
      settings = {
        APP_KEY_FILE = "/etc/speedtest-tracker-appkey";
      };
    };
  };

  testScript = ''
    sqlite.wait_for_unit("phpfpm-speedtest-tracker.service")
    sqlite.wait_for_unit("nginx.service")
    sqlite.wait_for_unit("speedtest-tracker-queue-worker.service")

    sqlite.succeed("curl -Ls -o /dev/null -w '%{http_code}' http://localhost/admin/login | grep '200'")
    sqlite.succeed("curl -Ls http://localhost/admin/login | grep -i 'login'")
    sqlite.succeed("systemctl start speedtest-tracker-scheduler.service")
  '';
}