diff --git a/inventories/default.nix b/inventories/default.nix index c720d20..c21931a 100644 --- a/inventories/default.nix +++ b/inventories/default.nix @@ -69,6 +69,13 @@ }; roles.default.machines.b4l = { }; }; + grafana = { + module = { + name = "grafana"; + input = "self"; + }; + roles.default.machines.b4l = { }; + }; }; }; }; diff --git a/machines/b4l/services/grafana.nix b/machines/b4l/services/grafana.nix new file mode 100644 index 0000000..85793da --- /dev/null +++ b/machines/b4l/services/grafana.nix @@ -0,0 +1,79 @@ +{ + pkgs, + config, + lib, + ... +}: +with lib; +let + serviceName = "${config.networking.hostName}-grafana"; + gfDomain = "${ + config.clan.core.vars.generators."${serviceName}".files.subdomain.value + }.${config.networking.fqdn}"; + + settingsFormatIni = pkgs.formats.ini { + listToValue = concatMapStringsSep " " (generators.mkValueStringDefault { }); + mkKeyValue = generators.mkKeyValueDefault { + mkValueString = v: if v == null then "" else generators.mkValueStringDefault { } v; + } "="; + }; + configFile = settingsFormatIni.generate "config.ini" config.services.grafana.settings; +in +{ + clan.core.vars.generators."${serviceName}" = { + files = { + adminpassword.secret = true; + subdomain.secret = false; + }; + prompts = { + subdomain = { + persist = true; + type = "line"; + description = "Sub-domain for Grafana. Default:(grafana)"; + }; + adminpassword = { + persist = true; + type = "hidden"; + description = "Password for the admin user. Leave empty to auto-generate."; + }; + }; + + runtimeInputs = [ + pkgs.xkcdpass + pkgs.coreutils + ]; + + script = '' + prompt_domain=$(cat "$prompts"/subdomain) + if [[ -n "''${prompt_domain-}" ]]; then + echo $prompt_domain | tr -d "\n" > "$out"/subdomain + else + echo -n "grafana" > "$out"/subdomain + fi + + prompt_password=$(cat "$prompts"/adminpassword) + if [[ -n "''${prompt_password-}" ]]; then + echo "$prompt_password" | tr -d "\n" > "$out"/adminpassword + else + xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > "$out"/adminpassword + fi + ''; + }; + + systemd.services.grafana.serviceConfig.ExecStartPre = [ + "+${pkgs.writeShellScript "grafana-set-password" '' + ${pkgs.grafana}/bin/grafana cli --homepath ${config.services.grafana.dataDir} --config ${configFile} admin reset-admin-password $(cat ${ + config.clan.core.vars.generators."${serviceName}".files.adminpassword.path + }) + ''}" + ]; + + services.nginx.virtualHosts."${gfDomain}" = { + forceSSL = true; + useACMEHost = "${config.networking.fqdn}"; + locations."/" = { + proxyPass = "http://localhost:${builtins.toString config.services.grafana.settings.server.http_port}"; + }; + }; + +} diff --git a/modules/clan/grafana/default.nix b/modules/clan/grafana/default.nix new file mode 100644 index 0000000..4ae21f3 --- /dev/null +++ b/modules/clan/grafana/default.nix @@ -0,0 +1,24 @@ +{ ... }: +{ + _class = "clan.service"; + manifest.name = "grafana"; + manifest.description = "Platform for data analytics and monitoring"; + manifest.categories = [ "System" ]; + + roles.default = { + + perInstance.nixosModule = + { + config, + lib, + ... + }: + { + services.grafana = { + enable = lib.mkDefault true; + }; + + clan.core.state.grafana.folders = [ config.services.grafana.dataDir ]; + }; + }; +} diff --git a/modules/clan/grafana/flake-module.nix b/modules/clan/grafana/flake-module.nix new file mode 100644 index 0000000..28fda45 --- /dev/null +++ b/modules/clan/grafana/flake-module.nix @@ -0,0 +1,18 @@ +{ lib, ... }: +let + module = lib.modules.importApply ./default.nix { }; +in +{ + clan.modules = { + grafana = module; + }; + perSystem = + { ... }: + { + clan.nixosTests.grafana = { + imports = [ ./tests/vm/default.nix ]; + + clan.modules."@clan/grafana" = module; + }; + }; +} diff --git a/modules/clan/grafana/tests/vm/default.nix b/modules/clan/grafana/tests/vm/default.nix new file mode 100644 index 0000000..51e1fe2 --- /dev/null +++ b/modules/clan/grafana/tests/vm/default.nix @@ -0,0 +1,42 @@ +{ + ... +}: +{ + name = "service-grafana"; + + clan = { + directory = ./.; + inventory = { + machines.server = { }; + + instances = { + grafana-test = { + module.name = "@clan/grafana"; + module.input = "self"; + roles.default.machines."server".settings = { }; + }; + }; + }; + }; + + nodes = { + server = { + services.grafana = { + settings = { + server.domain = "grafana.localhost"; + }; + }; + }; + }; + + testScript = '' + start_all() + + server.wait_for_unit("grafana") + + server.succeed("systemctl status grafana") + server.wait_for_open_port(3000) + server.succeed("curl -H \"Host: grafana.localhost\" http://127.0.0.1:3000 ") + server.succeed("grafana cli -v") + ''; +} diff --git a/vars/per-machine/b4l/b4l-grafana/adminpassword/machines/b4l b/vars/per-machine/b4l/b4l-grafana/adminpassword/machines/b4l new file mode 120000 index 0000000..72e1b85 --- /dev/null +++ b/vars/per-machine/b4l/b4l-grafana/adminpassword/machines/b4l @@ -0,0 +1 @@ +../../../../../../sops/machines/b4l \ No newline at end of file diff --git a/vars/per-machine/b4l/b4l-grafana/adminpassword/secret b/vars/per-machine/b4l/b4l-grafana/adminpassword/secret new file mode 100644 index 0000000..73005a5 --- /dev/null +++ b/vars/per-machine/b4l/b4l-grafana/adminpassword/secret @@ -0,0 +1,19 @@ +{ + "data": "ENC[AES256_GCM,data:9v56r2ZaEixMv61TGiCOmeAru2v9ZDUJe6v+Y5TdzfV5Rg==,iv:8kvq06/hfad+9af2PW+50l6Pzs99E5E2x8m3AIz5y90=,tag:aemnUje/OlGc6Hdzzkfdmg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJZUFaNW9GNXZSMnVjTG5v\nKzJlVWxzMnRSbC93a3BHc0t1YVlhQmRGOEZFCkFFK2NQNXJqdWd6cjFxaFRRR2Vy\nZE5tdkcwRXV6ZzVrdGdkcndCQ01KM3MKLS0tIEtDOG5qOG9MWWN4RnZuVGF5ajJN\nMjk0TkJvR3duZHhGTjQ2WVpqRmkrOWsKjPbLas4eDUXdhZyE29AXklDDM+czo2b7\nqvOAY2c+TyMatGpMRMPogUqGC9mj5jTZe+ZfHcxIfytDXbOgldRPow==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1sg0rvgyetdcqw7j2x983fh69kdkvqsngpe5x36e5920qa7fze3cqhj4wgx", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtZ3NrbEEyMUpoQTRFL2ZT\nMXNrUTZsSWV3aWRMdGZUNXV2azAwOUJ3cFFRClNmMCtVTXF0VU9PM0NxTWtWSXBh\nMDVlc3BpS3ZVSUlEQit4c21IZy85QW8KLS0tIEMvWWhYM3VyTWk0TnRJbHhublpa\nZEhKSitydFdMbmVWZ01mdlVtSWV6S2sKY7L+wbtpIlo00xQukuj1Fv4iKpC3BftP\n4+55dUhp9cc7VxqV/3TkfPxeyr95OltVZOHhhwSnEzcyCP9XQ16DIw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2025-08-01T09:25:47Z", + "mac": "ENC[AES256_GCM,data:nEaCXu003Y/kwtNrBT+reXOZfTOjhqxUXLvUFa4RFrFREgxIsNkqi80GyDFAaBbgAFA5B71Ozwh8Ml0g3TJ15DsoLkaUgrLR9gXIuh4FoDpFKmvFzUT7nm4Ac003b2bkaMPVNXRU24O+JVgK9U5tmKfqmEkRnlY5MTLFO5EGgnY=,iv:3RpMM6CjtWJSqY8T++2GIV4b3kN724h/ZhuS1U8lJGY=,tag:tkL5+aU06xF4LUeMoq6qoQ==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} diff --git a/vars/per-machine/b4l/b4l-grafana/adminpassword/users/kurogeek b/vars/per-machine/b4l/b4l-grafana/adminpassword/users/kurogeek new file mode 120000 index 0000000..970aefa --- /dev/null +++ b/vars/per-machine/b4l/b4l-grafana/adminpassword/users/kurogeek @@ -0,0 +1 @@ +../../../../../../sops/users/kurogeek \ No newline at end of file diff --git a/vars/per-machine/b4l/b4l-grafana/subdomain/value b/vars/per-machine/b4l/b4l-grafana/subdomain/value new file mode 100644 index 0000000..6da7003 --- /dev/null +++ b/vars/per-machine/b4l/b4l-grafana/subdomain/value @@ -0,0 +1 @@ +grafana \ No newline at end of file