diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix
index 58932ecdc51..8cc8ea9d9b6 100644
--- a/maintainers/maintainer-list.nix
+++ b/maintainers/maintainer-list.nix
@@ -9821,6 +9821,17 @@
githubId = 645664;
name = "Philippe Hürlimann";
};
+ phaer = {
+ name = "Paul Haerle";
+ email = "nix@phaer.org";
+
+ matrix = "@phaer:matrix.org";
+ github = "phaer";
+ githubId = 101753;
+ keys = [{
+ fingerprint = "5D69 CF04 B7BC 2BC1 A567 9267 00BC F29B 3208 0700";
+ }];
+ };
philandstuff = {
email = "philip.g.potter@gmail.com";
github = "philandstuff";
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 759c31ef28b..03442d50503 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1057,6 +1057,7 @@
./services/web-apps/gerrit.nix
./services/web-apps/gotify-server.nix
./services/web-apps/grocy.nix
+ ./services/web-apps/healthchecks.nix
./services/web-apps/hedgedoc.nix
./services/web-apps/hledger-web.nix
./services/web-apps/icingaweb2/icingaweb2.nix
diff --git a/nixos/modules/services/web-apps/healthchecks.nix b/nixos/modules/services/web-apps/healthchecks.nix
new file mode 100644
index 00000000000..be025e7dd55
--- /dev/null
+++ b/nixos/modules/services/web-apps/healthchecks.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, buildEnv, ... }:
+
+with lib;
+
+let
+ defaultUser = "healthchecks";
+ cfg = config.services.healthchecks;
+ pkg = cfg.package;
+ boolToPython = b: if b then "True" else "False";
+ environment = {
+ PYTHONPATH = pkg.pythonPath;
+ STATIC_ROOT = cfg.dataDir + "/static";
+ DB_NAME = "${cfg.dataDir}/healthchecks.sqlite";
+ } // cfg.settings;
+
+ environmentFile = pkgs.writeText "healthchecks-environment" (lib.generators.toKeyValue { } environment);
+
+ healthchecksManageScript = with pkgs; (writeShellScriptBin "healthchecks-manage" ''
+ if [[ "$USER" != "${cfg.user}" ]]; then
+ echo "please run as user 'healtchecks'." >/dev/stderr
+ exit 1
+ fi
+ export $(cat ${environmentFile} | xargs);
+ exec ${pkg}/opt/healthchecks/manage.py "$@"
+ '');
+in
+{
+ options.services.healthchecks = {
+ enable = mkEnableOption "healthchecks" // {
+ description = ''
+ Enable healthchecks.
+ It is expected to be run behind a HTTP reverse proxy.
+ '';
+ };
+
+ package = mkOption {
+ default = pkgs.healthchecks;
+ defaultText = literalExpression "pkgs.healthchecks";
+ type = types.package;
+ description = "healthchecks package to use.";
+ };
+
+ user = mkOption {
+ default = defaultUser;
+ type = types.str;
+ description = ''
+ User account under which healthchecks runs.
+
+
+ If left as the default value this user will automatically be created
+ on system activation, otherwise you are responsible for
+ ensuring the user exists before the healthchecks service starts.
+
+ '';
+ };
+
+ group = mkOption {
+ default = defaultUser;
+ type = types.str;
+ description = ''
+ Group account under which healthchecks runs.
+
+
+ If left as the default value this group will automatically be created
+ on system activation, otherwise you are responsible for
+ ensuring the group exists before the healthchecks service starts.
+
+ '';
+ };
+
+ listenAddress = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = "Address the server will listen on.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 8000;
+ description = "Port the server will listen on.";
+ };
+
+ dataDir = mkOption {
+ type = types.str;
+ default = "/var/lib/healthchecks";
+ description = ''
+ The directory used to store all data for healthchecks.
+
+
+ If left as the default value this directory will automatically be created before
+ the healthchecks server starts, otherwise you are responsible for ensuring the
+ directory exists with appropriate ownership and permissions.
+
+ '';
+ };
+
+ settings = lib.mkOption {
+ description = ''
+ Environment variables which are read by healthchecks (local)_settings.py.
+
+ Settings which are explictly covered in options bewlow, are type-checked and/or transformed
+ before added to the environment, everything else is passed as a string.
+
+ See https://healthchecks.io/docs/self_hosted_configuration/
+ for a full documentation of settings.
+
+ We add two variables to this list inside the packages local_settings.py.
+ - STATIC_ROOT to set a state directory for dynamically generated static files.
+ - SECRET_KEY_FILE to read SECRET_KEY from a file at runtime and keep it out of /nix/store.
+ '';
+ type = types.submodule {
+ freeformType = types.attrsOf types.str;
+ options = {
+ ALLOWED_HOSTS = lib.mkOption {
+ type = types.listOf types.str;
+ default = [ "*" ];
+ description = "The host/domain names that this site can serve.";
+ apply = lib.concatStringsSep ",";
+ };
+
+ SECRET_KEY_FILE = mkOption {
+ type = types.path;
+ description = "Path to a file containing the secret key.";
+ };
+
+ DEBUG = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Enable debug mode.";
+ apply = boolToPython;
+ };
+
+ REGISTRATION_OPEN = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ A boolean that controls whether site visitors can create new accounts.
+ Set it to false if you are setting up a private Healthchecks instance,
+ but it needs to be publicly accessible (so, for example, your cloud
+ services can send pings to it).
+ If you close new user registration, you can still selectively invite
+ users to your team account.
+ '';
+ apply = boolToPython;
+ };
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [ healthchecksManageScript ];
+
+ systemd.targets.healthchecks = {
+ description = "Target for all Healthchecks services";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" "network-online.target" ];
+ };
+
+ systemd.services =
+ let
+ commonConfig = {
+ WorkingDirectory = cfg.dataDir;
+ User = cfg.user;
+ Group = cfg.group;
+ EnvironmentFile = environmentFile;
+ StateDirectory = mkIf (cfg.dataDir == "/var/lib/healthchecks") "healthchecks";
+ StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750";
+ };
+ in
+ {
+ healthchecks-migration = {
+ description = "Healthchecks migrations";
+ wantedBy = [ "healthchecks.target" ];
+
+ serviceConfig = commonConfig // {
+ Restart = "on-failure";
+ Type = "oneshot";
+ ExecStart = ''
+ ${pkg}/opt/healthchecks/manage.py migrate
+ '';
+ };
+ };
+
+ healthchecks = {
+ description = "Healthchecks WSGI Service";
+ wantedBy = [ "healthchecks.target" ];
+ after = [ "healthchecks-migration.service" ];
+
+ preStart = ''
+ ${pkg}/opt/healthchecks/manage.py collectstatic --no-input
+ ${pkg}/opt/healthchecks/manage.py remove_stale_contenttypes --no-input
+ ${pkg}/opt/healthchecks/manage.py compress
+ '';
+
+ serviceConfig = commonConfig // {
+ Restart = "always";
+ ExecStart = ''
+ ${pkgs.python3Packages.gunicorn}/bin/gunicorn hc.wsgi \
+ --bind ${cfg.listenAddress}:${toString cfg.port} \
+ --pythonpath ${pkg}/opt/healthchecks
+ '';
+ };
+ };
+
+ healthchecks-sendalerts = {
+ description = "Healthchecks Alert Service";
+ wantedBy = [ "healthchecks.target" ];
+ after = [ "healthchecks.service" ];
+
+ serviceConfig = commonConfig // {
+ Restart = "always";
+ ExecStart = ''
+ ${pkg}/opt/healthchecks/manage.py sendalerts
+ '';
+ };
+ };
+
+ healthchecks-sendreports = {
+ description = "Healthchecks Reporting Service";
+ wantedBy = [ "healthchecks.target" ];
+ after = [ "healthchecks.service" ];
+
+ serviceConfig = commonConfig // {
+ Restart = "always";
+ ExecStart = ''
+ ${pkg}/opt/healthchecks/manage.py sendreports --loop
+ '';
+ };
+ };
+ };
+
+ users.users = optionalAttrs (cfg.user == defaultUser) {
+ ${defaultUser} =
+ {
+ description = "healthchecks service owner";
+ isSystemUser = true;
+ group = defaultUser;
+ };
+ };
+
+ users.groups = optionalAttrs (cfg.user == defaultUser) {
+ ${defaultUser} =
+ {
+ members = [ defaultUser ];
+ };
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 671b1a8876a..6a72130be5f 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -203,6 +203,7 @@ in {
haste-server = handleTest ./haste-server.nix {};
haproxy = handleTest ./haproxy.nix {};
hardened = handleTest ./hardened.nix {};
+ healthchecks = handleTest ./web-apps/healthchecks.nix {};
hbase1 = handleTest ./hbase.nix { package=pkgs.hbase1; };
hbase2 = handleTest ./hbase.nix { package=pkgs.hbase2; };
hbase3 = handleTest ./hbase.nix { package=pkgs.hbase3; };
diff --git a/nixos/tests/web-apps/healthchecks.nix b/nixos/tests/web-apps/healthchecks.nix
new file mode 100644
index 00000000000..41374f5e314
--- /dev/null
+++ b/nixos/tests/web-apps/healthchecks.nix
@@ -0,0 +1,42 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }: {
+ name = "healthchecks";
+
+ meta = with lib.maintainers; {
+ maintainers = [ phaer ];
+ };
+
+ nodes.machine = { ... }: {
+ services.healthchecks = {
+ enable = true;
+ settings = {
+ SITE_NAME = "MyUniqueInstance";
+ COMPRESS_ENABLED = "True";
+ SECRET_KEY_FILE = pkgs.writeText "secret"
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ };
+ };
+ };
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_unit("healthchecks.target")
+ machine.wait_until_succeeds("journalctl --since -1m --unit healthchecks --grep Listening")
+
+ with subtest("Home screen loads"):
+ machine.succeed(
+ "curl -sSfL http://localhost:8000 | grep '
Sign In'"
+ )
+
+ with subtest("Setting SITE_NAME via freeform option works"):
+ machine.succeed(
+ "curl -sSfL http://localhost:8000 | grep 'MyUniqueInstance'"
+ )
+
+ with subtest("Manage script works"):
+ # Should fail if not called by healthchecks user
+ machine.fail("echo 'print(\"foo\")' | healthchecks-manage help")
+
+ # "shell" sucommand should succeed, needs python in PATH.
+ assert "foo\n" == machine.succeed("echo 'print(\"foo\")' | sudo -u healthchecks healthchecks-manage shell")
+ '';
+})
diff --git a/pkgs/development/python-modules/cron-descriptor/default.nix b/pkgs/development/python-modules/cron-descriptor/default.nix
new file mode 100644
index 00000000000..582ef48199f
--- /dev/null
+++ b/pkgs/development/python-modules/cron-descriptor/default.nix
@@ -0,0 +1,36 @@
+{ lib
+, python
+, buildPythonPackage
+, fetchFromGitHub
+, pytestCheckHook
+}:
+
+buildPythonPackage rec {
+ pname = "cron_descriptor";
+ version = "1.2.24";
+
+ src = fetchFromGitHub {
+ owner = "Salamek";
+ repo = "cron-descriptor";
+ rev = version;
+ sha256 = "sha256-Gf7n8OiFuaN+8MqsXSg9RBPh2gXfPgjJ4xeuinGYKMw=";
+ };
+
+ # remove tests_require, as we don't do linting anyways
+ postPatch = ''
+ sed -i "/'pep8\|flake8\|pep8-naming',/d" setup.py
+ '';
+
+ checkPhase = ''
+ ${python.interpreter} setup.py test
+ '';
+
+ pythonImportsCheck = [ "cron_descriptor" ];
+
+ meta = with lib; {
+ description = "Library that converts cron expressions into human readable strings";
+ homepage = "https://github.com/Salamek/cron-descriptor";
+ license = licenses.mit;
+ maintainers = with maintainers; [ phaer ];
+ };
+}
diff --git a/pkgs/development/python-modules/cronsim/default.nix b/pkgs/development/python-modules/cronsim/default.nix
new file mode 100644
index 00000000000..35208bbd7b7
--- /dev/null
+++ b/pkgs/development/python-modules/cronsim/default.nix
@@ -0,0 +1,28 @@
+{ lib
+, buildPythonPackage
+, fetchPypi
+, pytestCheckHook
+}:
+
+buildPythonPackage rec {
+ pname = "cronsim";
+ version = "2.1";
+
+ src = fetchPypi {
+ inherit pname version;
+ sha256 = "sha256-nwlSAbD+y0l9jyVSVShzWeC7nC5RZRD/kAhCi3Nd9xY=";
+ };
+
+ checkInputs = [
+ pytestCheckHook
+ ];
+
+ pythonImportsCheck = [ "cronsim" ];
+
+ meta = with lib; {
+ description = "Cron expression parser and evaluator";
+ homepage = "https://github.com/cuu508/cronsim";
+ license = licenses.bsd3;
+ maintainers = with maintainers; [ phaer ];
+ };
+}
diff --git a/pkgs/development/python-modules/segno/default.nix b/pkgs/development/python-modules/segno/default.nix
new file mode 100644
index 00000000000..5db60d0d0bd
--- /dev/null
+++ b/pkgs/development/python-modules/segno/default.nix
@@ -0,0 +1,34 @@
+{ lib
+, buildPythonPackage
+, fetchFromGitHub
+, pytestCheckHook
+, pypng
+, pyzbar
+}:
+
+buildPythonPackage rec {
+ pname = "segno";
+ version = "1.5.2";
+
+ src = fetchFromGitHub {
+ owner = "heuer";
+ repo = "segno";
+ rev = version;
+ sha256 = "sha256-+OEXG5OvrZ5Ft7IO/7zodf+SgiRF+frwjltrBENNnHo=";
+ };
+
+ checkInputs = [
+ pytestCheckHook
+ pypng
+ pyzbar
+ ];
+
+ pythonImportsCheck = [ "segno" ];
+
+ meta = with lib; {
+ description = "QR Code and Micro QR Code encoder";
+ homepage = "https://github.com/heuer/segno/";
+ license = licenses.bsd3;
+ maintainers = with maintainers; [ phaer ];
+ };
+}
diff --git a/pkgs/servers/web-apps/healthchecks/default.nix b/pkgs/servers/web-apps/healthchecks/default.nix
new file mode 100644
index 00000000000..aef269907ac
--- /dev/null
+++ b/pkgs/servers/web-apps/healthchecks/default.nix
@@ -0,0 +1,84 @@
+{ lib
+, writeText
+, fetchFromGitHub
+, nixosTests
+, python3
+}:
+let
+ py = python3.override {
+ packageOverrides = final: prev: {
+ django = prev.django_4;
+ fido2 = prev.fido2.overridePythonAttrs (old: rec {
+ version = "0.9.3";
+ src = prev.fetchPypi {
+ pname = "fido2";
+ inherit version;
+ sha256 = "sha256-tF6JphCc/Lfxu1E3dqotZAjpXEgi+DolORi5RAg0Zuw=";
+ };
+ });
+ };
+ };
+in
+py.pkgs.buildPythonApplication rec {
+ pname = "healthchecks";
+ version = "2.2.1";
+ format = "other";
+
+ src = fetchFromGitHub {
+ owner = "healthchecks";
+ repo = pname;
+ rev = "v${version}";
+ sha256 = "sha256-C+NUvs5ijbj/l8G1sjSXvUJDNSOTVFAStfS5KtYFpUs=";
+ };
+
+ propagatedBuildInputs = with py.pkgs; [
+ apprise
+ cffi
+ cron-descriptor
+ cronsim
+ cryptography
+ django
+ django_compressor
+ fido2
+ minio
+ psycopg2
+ py
+ pyotp
+ requests
+ segno
+ statsd
+ whitenoise
+ ];
+
+ localSettings = writeText "local_settings.py" ''
+ import os
+ STATIC_ROOT = os.getenv("STATIC_ROOT")
+ SECRET_KEY_FILE = os.getenv("SECRET_KEY_FILE")
+ if SECRET_KEY_FILE:
+ with open(SECRET_KEY_FILE, "r") as file:
+ SECRET_KEY = file.readline()
+ '';
+
+ installPhase = ''
+ mkdir -p $out/opt/healthchecks
+ cp -r . $out/opt/healthchecks
+ chmod +x $out/opt/healthchecks/manage.py
+ cp ${localSettings} $out/opt/healthchecks/hc/local_settings.py
+ '';
+
+ passthru = {
+ # PYTHONPATH of all dependencies used by the package
+ pythonPath = py.pkgs.makePythonPath propagatedBuildInputs;
+
+ tests = {
+ inherit (nixosTests) healthchecks;
+ };
+ };
+
+ meta = with lib; {
+ homepage = "https://github.com/healthchecks/healthchecks";
+ description = "A cron monitoring tool written in Python & Django ";
+ license = licenses.bsd3;
+ maintainers = with maintainers; [ phaer ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 7e164b393ba..f2d5472383b 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -7148,6 +7148,8 @@ with pkgs;
buildGoModule = buildGo118Module;
};
+ healthchecks = callPackage ../servers/web-apps/healthchecks { };
+
heisenbridge = callPackage ../servers/heisenbridge { };
helio-workstation = callPackage ../applications/audio/helio-workstation { };
diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix
index bbffc10a2de..3eb09abe3e4 100644
--- a/pkgs/top-level/python-packages.nix
+++ b/pkgs/top-level/python-packages.nix
@@ -1974,8 +1974,12 @@ in {
criticality-score = callPackage ../development/python-modules/criticality-score { };
+ cron-descriptor = callPackage ../development/python-modules/cron-descriptor { };
+
croniter = callPackage ../development/python-modules/croniter { };
+ cronsim = callPackage ../development/python-modules/cronsim { };
+
crossplane = callPackage ../development/python-modules/crossplane { };
crownstone-cloud = callPackage ../development/python-modules/crownstone-cloud { };
@@ -9585,6 +9589,8 @@ in {
segments = callPackage ../development/python-modules/segments { };
+ segno = callPackage ../development/python-modules/segno { };
+
segyio = toPythonModule (callPackage ../development/python-modules/segyio {
inherit (pkgs) cmake ninja;
});