diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 269f9fa56191..0ef44a7ded92 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -123,6 +123,8 @@ - [vivid](https://github.com/sharkdp/vivid), a generator for LS_COLOR. Available as [programs.vivid](#opt-programs.vivid.enable). +- [matrix-alertmanager](https://github.com/jaywink/matrix-alertmanager), a bot to receive Alertmanager webhook events and forward them to chosen Matrix rooms. Available as [services.matrix-alertmanager](options.html#opt-services.matrix-alertmanager.enable). + - [waagent](https://github.com/Azure/WALinuxAgent), the Microsoft Azure Linux Agent (waagent) manages Linux provisioning and VM interaction with the Azure Fabric Controller. Available with [services.waagent](options.html#opt-services.waagent.enable). - [nfc-nci](https://github.com/StarGate01/ifdnfc-nci), an alternative NFC stack and PC/SC driver for the NXP PN54x chipset, commonly found in Lenovo systems as NXP1001 (NPC300). Available as [hardware.nfc-nci](#opt-hardware.nfc-nci.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3918a875ad32..0be7e4c72f84 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -742,6 +742,7 @@ ./services/matrix/dendrite.nix ./services/matrix/hebbot.nix ./services/matrix/hookshot.nix + ./services/matrix/matrix-alertmanager.nix ./services/matrix/maubot.nix ./services/matrix/mautrix-meta.nix ./services/matrix/mautrix-signal.nix diff --git a/nixos/modules/services/matrix/matrix-alertmanager.nix b/nixos/modules/services/matrix/matrix-alertmanager.nix new file mode 100644 index 000000000000..a00f808d0b66 --- /dev/null +++ b/nixos/modules/services/matrix/matrix-alertmanager.nix @@ -0,0 +1,124 @@ +{ + lib, + config, + pkgs, + ... +}: +let + cfg = config.services.matrix-alertmanager; + rooms = room: lib.concatStringsSep "/" (room.receivers ++ [ room.roomId ]); + concatenatedRooms = lib.concatStringsSep "|" (map rooms cfg.matrixRooms); +in +{ + meta.maintainers = [ lib.maintainers.erethon ]; + + options.services.matrix-alertmanager = { + enable = lib.mkEnableOption "matrix-alertmanager"; + package = lib.mkPackageOption pkgs "matrix-alertmanager" { }; + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "Port that matrix-alertmanager listens on."; + }; + homeserverUrl = lib.mkOption { + type = lib.types.str; + description = "URL of the Matrix homeserver to use."; + example = "https://matrix.example.com"; + }; + matrixUser = lib.mkOption { + type = lib.types.str; + description = "Matrix user to use for the bot."; + example = "@alertmanageruser:example.com"; + }; + matrixRooms = lib.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + receivers = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "List of receivers for this room"; + }; + roomId = lib.mkOption { + type = lib.types.str; + description = "Matrix room ID"; + apply = + x: + assert lib.assertMsg (lib.hasPrefix "!" x) "Matrix room ID must start with a '!'. Got: ${x}"; + x; + }; + }; + } + ); + description = '' + Combination of Alertmanager receiver(s) and rooms for the bot to join. + Each Alertmanager receiver can be mapped to post to a matrix room. + + Note, you must use a room ID and not a room alias/name. Room IDs start + with a "!". + ''; + example = [ + { + receivers = [ + "receiver1" + "receiver2" + ]; + roomId = "!roomid@example.com"; + } + { + receivers = [ "receiver3" ]; + roomId = "!differentroomid@example.com"; + } + ]; + }; + mention = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Makes the bot mention @room when posting an alert"; + }; + tokenFile = lib.mkOption { + type = lib.types.pathWith { + inStore = false; + absolute = true; + }; + description = "File that contains a valid Matrix token for the Matrix user."; + }; + secretFile = lib.mkOption { + type = lib.types.pathWith { + inStore = false; + absolute = true; + }; + description = "File that contains a secret for the Alertmanager webhook."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.matrix-alertmanager = { + description = "A bot to receive Alertmanager webhook events and forward them to chosen rooms."; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + DynamicUser = true; + Restart = "always"; + RestartSec = "10s"; + LoadCredential = [ + "token:${cfg.tokenFile}" + "secret:${cfg.secretFile}" + ]; + }; + + environment = { + APP_PORT = toString cfg.port; + MATRIX_HOMESERVER_URL = cfg.homeserverUrl; + MATRIX_ROOMS = concatenatedRooms; + MATRIX_USER = cfg.matrixUser; + MENTION_ROOM = if cfg.mention then "1" else "0"; + }; + + script = '' + export APP_ALERTMANAGER_SECRET=$(cat "''${CREDENTIALS_DIRECTORY}/secret") + export MATRIX_TOKEN=$(cat "''${CREDENTIALS_DIRECTORY}/token") + exec ${lib.getExe cfg.package} + ''; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 999bfd08af55..ed753749c8d6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -643,6 +643,7 @@ in { mate-wayland = handleTest ./mate-wayland.nix {}; matter-server = handleTest ./matter-server.nix {}; matomo = runTest ./matomo.nix; + matrix-alertmanager = runTest ./matrix/matrix-alertmanager.nix; matrix-appservice-irc = runTest ./matrix/appservice-irc.nix; matrix-conduit = handleTest ./matrix/conduit.nix {}; matrix-synapse = handleTest ./matrix/synapse.nix {}; diff --git a/nixos/tests/matrix/matrix-alertmanager.nix b/nixos/tests/matrix/matrix-alertmanager.nix new file mode 100644 index 000000000000..4e5bac450da2 --- /dev/null +++ b/nixos/tests/matrix/matrix-alertmanager.nix @@ -0,0 +1,132 @@ +{ pkgs, ... }: +let + secret-files = pkgs.runCommandLocal "secret-files" { } '' + mkdir -p $out + echo -n faketoken > $out/token.txt + echo -n wontbeused > $out/secret.txt + ''; +in +{ + name = "matrix-alertmanager"; + meta.maintainers = with pkgs.lib.maintainers; [ erethon ]; + + nodes = { + homeserver = + { pkgs, ... }: + { + services.matrix-synapse = { + enable = true; + settings = { + database.name = "sqlite3"; + tls_certificate_path = "../common/acme/server/acme.test.cert.pem"; + tls_private_key_path = "../common/acme/server/acme.test.key.pem"; + enable_registration = true; + enable_registration_without_verification = true; + registration_shared_secret = "supersecret-registration"; + listeners = [ + { + # The default but tls=false + bind_addresses = [ + "0.0.0.0" + ]; + port = 8448; + resources = [ + { + compress = true; + names = [ "client" ]; + } + { + compress = false; + names = [ "federation" ]; + } + ]; + tls = false; + type = "http"; + x_forwarded = false; + } + ]; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8448 ]; + + environment.systemPackages = [ + (pkgs.writeShellScriptBin "register_alertmanager_user" '' + exec ${pkgs.matrix-synapse}/bin/register_new_matrix_user \ + -u alertmanager \ + -p alertmanager-password \ + --admin \ + --shared-secret supersecret-registration \ + http://localhost:8448 + '') + # This is needed to solve a chicken and egg + # problem. Matrix-alertmanager expects a token for authentication, + # but a token is created after the user has been registered. This + # changes the token in the database to match the one specified in + # the service settings. + (pkgs.writers.writePython3Bin "hardcode_matrix_values" + { + libraries = with pkgs.python3Packages; [ + sqlite-utils + ]; + } + '' + import sqlite3 + con = sqlite3.connect("/var/lib/matrix-synapse/homeserver.db") + cur = con.cursor() + cur.execute( + "update access_tokens set token='%s' where user_id = '%s'" + % ("faketoken", "@alertmanager:homeserver") + ) + con.commit() + con.close() + '' + ) + ]; + }; + + matrix_alertmanager = + { config, pkgs, ... }: + { + environment.etc.token-file.source = "${secret-files}/token.txt"; + environment.etc.secret-file.source = "${secret-files}/secret.txt"; + services.matrix-alertmanager = { + enable = true; + tokenFile = "/etc/${config.environment.etc.token-file.target}"; + secretFile = "/etc/${config.environment.etc.secret-file.target}"; + homeserverUrl = "http://homeserver:8448"; + # Matrix-alertmanager expects at least a room in its configuration + # in order to start. However, the room doesn't have to exist for + # matrix-alertmanager to start, so this is a configuration only + # placeholder. + matrixRooms = [ + { + receivers = [ "matrix" ]; + roomId = "!room_id:homeserver"; + } + ]; + matrixUser = "alertmanager"; + }; + }; + }; + + testScript = '' + with subtest("start homeserver"): + homeserver.start() + homeserver.wait_for_unit("matrix-synapse.service") + homeserver.wait_until_succeeds("curl --fail -L http://localhost:8448/") + + with subtest("register user"): + # register alertmanager user + homeserver.succeed("register_alertmanager_user") + + with subtest("hardcode matrix values for matrix-alertmanager to use"): + homeserver.succeed("hardcode_matrix_values") + + with subtest("start matrix_alertmanager"): + matrix_alertmanager.start() + matrix_alertmanager.wait_for_unit("matrix-alertmanager.service") + matrix_alertmanager.wait_until_succeeds("curl --fail -L http://localhost:3000/") + matrix_alertmanager.wait_for_console_text("matrix-alertmanager initialized and ready") + ''; +}