diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 1abf87dfcc63f9..a6c1d7c5d66cf0 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -806,6 +806,7 @@ ./services/web-apps/gotify-server.nix ./services/web-apps/icingaweb2/icingaweb2.nix ./services/web-apps/icingaweb2/module-monitoring.nix + ./services/web-apps/ihatemoney ./services/web-apps/limesurvey.nix ./services/web-apps/mattermost.nix ./services/web-apps/mediawiki.nix diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix new file mode 100644 index 00000000000000..68769ac8c03161 --- /dev/null +++ b/nixos/modules/services/web-apps/ihatemoney/default.nix @@ -0,0 +1,141 @@ +{ config, pkgs, lib, ... }: +with lib; +let + cfg = config.services.ihatemoney; + user = "ihatemoney"; + group = "ihatemoney"; + db = "ihatemoney"; + python3 = config.services.uwsgi.package.python3; + pkg = python3.pkgs.ihatemoney; + toBool = x: if x then "True" else "False"; + configFile = pkgs.writeText "ihatemoney.cfg" '' + from secrets import token_hex + # load a persistent secret key + SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key" + SECRET_KEY = "" + try: + with open(SECRET_KEY_FILE) as f: + SECRET_KEY = f.read() + except FileNotFoundError: + pass + if not SECRET_KEY: + print("ihatemoney: generating a new secret key") + SECRET_KEY = token_hex(50) + with open(SECRET_KEY_FILE, "w") as f: + f.write(SECRET_KEY) + del token_hex + del SECRET_KEY_FILE + + # "normal" configuration + DEBUG = False + SQLALCHEMY_DATABASE_URI = '${ + if cfg.backend == "sqlite" + then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite" + else "postgresql:///${db}"}' + SQLALCHEMY_TRACK_MODIFICATIONS = False + MAIL_DEFAULT_SENDER = ("${cfg.defaultSender.name}", "${cfg.defaultSender.email}") + ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject} + ADMIN_PASSWORD = "${toString cfg.adminHashedPassword /*toString null == ""*/}" + ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation} + ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard} + + ${cfg.extraConfig} + ''; +in + { + options.services.ihatemoney = { + enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root"; + backend = mkOption { + type = types.enum [ "sqlite" "postgresql" ]; + default = "sqlite"; + description = '' + The database engine to use for ihatemoney. + If postgresql is selected, then a database called + ${db} will be created. If you disable this option, + it will however not be removed. + ''; + }; + adminHashedPassword = mkOption { + type = types.nullOr types.str; + default = null; + description = "The hashed password of the administrator. To obtain it, run ihatemoney generate_password_hash"; + }; + uwsgiConfig = mkOption { + type = types.attrs; + example = { + http = ":8000"; + }; + description = "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen."; + }; + defaultSender = { + name = mkOption { + type = types.str; + default = "Budget manager"; + description = "The display name of the sender of ihatemoney emails"; + }; + email = mkOption { + type = types.str; + default = "ihatemoney@${config.networking.hostName}"; + description = "The email of the sender of ihatemoney emails"; + }; + }; + enableDemoProject = mkEnableOption "access to the demo project in ihatemoney"; + enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone"; + enableAdminDashboard = mkEnableOption "ihatemoney admin dashboard"; + extraConfig = mkOption { + type = types.str; + default = ""; + description = "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation."; + }; + }; + config = mkIf cfg.enable { + services.postgresql = mkIf (cfg.backend == "postgresql") { + enable = true; + ensureDatabases = [ db ]; + ensureUsers = [ { + name = user; + ensurePermissions = { + "DATABASE ${db}" = "ALL PRIVILEGES"; + }; + } ]; + }; + systemd.services.postgresql = mkIf (cfg.backend == "postgresql") { + wantedBy = [ "uwsgi.service" ]; + before = [ "uwsgi.service" ]; + }; + systemd.tmpfiles.rules = [ + "d /var/lib/ihatemoney 770 ${user} ${group}" + ]; + users = { + users.${user} = { + isSystemUser = true; + inherit group; + }; + groups.${group} = {}; + }; + services.uwsgi = { + enable = true; + plugins = [ "python3" ]; + # the vassal needs to be able to setuid + user = "root"; + group = "root"; + instance = { + type = "emperor"; + vassals.ihatemoney = { + type = "normal"; + strict = true; + uid = user; + gid = group; + # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c + enable-threads = true; + module = "wsgi:application"; + chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney"; + env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ]; + pythonPackages = self: [ self.ihatemoney ]; + } // cfg.uwsgiConfig; + }; + }; + }; + } + + diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix index 0c727cf44aeed9..3481b5e6040339 100644 --- a/nixos/modules/services/web-servers/uwsgi.nix +++ b/nixos/modules/services/web-servers/uwsgi.nix @@ -5,10 +5,6 @@ with lib; let cfg = config.services.uwsgi; - uwsgi = pkgs.uwsgi.override { - plugins = cfg.plugins; - }; - buildCfg = name: c: let plugins = @@ -23,8 +19,8 @@ let python = if hasPython2 && hasPython3 then throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3" - else if hasPython2 then uwsgi.python2 - else if hasPython3 then uwsgi.python3 + else if hasPython2 then cfg.package.python2 + else if hasPython3 then cfg.package.python3 else null; pythonEnv = python.withPackages (c.pythonPackages or (self: [])); @@ -77,6 +73,11 @@ in { description = "Where uWSGI communication sockets can live"; }; + package = mkOption { + type = types.package; + internal = true; + }; + instance = mkOption { type = types.attrs; default = { @@ -138,7 +139,7 @@ in { ''; serviceConfig = { Type = "notify"; - ExecStart = "${uwsgi}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json"; + ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; NotifyAccess = "main"; @@ -156,5 +157,9 @@ in { users.groups = optionalAttrs (cfg.group == "uwsgi") { uwsgi.gid = config.ids.gids.uwsgi; }; + + services.uwsgi.package = pkgs.uwsgi.override { + inherit (cfg) plugins; + }; }; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 3f6921e0f4dd59..fe9c4df1416fec 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -122,6 +122,7 @@ in i3wm = handleTest ./i3wm.nix {}; icingaweb2 = handleTest ./icingaweb2.nix {}; iftop = handleTest ./iftop.nix {}; + ihatemoney = handleTest ./ihatemoney.nix {}; incron = handleTest ./incron.nix {}; influxdb = handleTest ./influxdb.nix {}; initrd-network-ssh = handleTest ./initrd-network-ssh {}; diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix new file mode 100644 index 00000000000000..14db17fe5e6768 --- /dev/null +++ b/nixos/tests/ihatemoney.nix @@ -0,0 +1,52 @@ +{ system ? builtins.currentSystem +, config ? {} +, pkgs ? import ../.. { inherit system config; } +}: + +let + inherit (import ../lib/testing.nix { inherit system pkgs; }) makeTest; +in +map ( + backend: makeTest { + name = "ihatemoney-${backend}"; + machine = { lib, ... }: { + services.ihatemoney = { + enable = true; + enablePublicProjectCreation = true; + inherit backend; + uwsgiConfig = { + http = ":8000"; + }; + }; + boot.cleanTmpDir = true; + # ihatemoney needs a local smtp server otherwise project creation just crashes + services.opensmtpd = { + enable = true; + serverConfiguration = '' + listen on lo + action foo relay + match from any for any action foo + ''; + }; + }; + testScript = '' + $machine->waitForOpenPort(8000); + $machine->waitForUnit("uwsgi.service"); + my $return = $machine->succeed("curl -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay\@example.com'"); + die "wrong project id $return" unless "\"yay\"\n" eq $return; + my $timestamp = $machine->succeed("stat --printf %Y /var/lib/ihatemoney/secret_key"); + my $owner = $machine->succeed("stat --printf %U:%G /var/lib/ihatemoney/secret_key"); + die "wrong ownership for the secret key: $owner, is uwsgi running as the right user ?" unless $owner eq "ihatemoney:ihatemoney"; + $machine->shutdown(); + $machine->start(); + $machine->waitForOpenPort(8000); + $machine->waitForUnit("uwsgi.service"); + # check that the database is really persistent + print $machine->succeed("curl --basic -u yay:yay http://localhost:8000/api/projects/yay"); + # check that the secret key is really persistent + my $timestamp2 = $machine->succeed("stat --printf %Y /var/lib/ihatemoney/secret_key"); + die unless $timestamp eq $timestamp2; + $machine->succeed("curl http://localhost:8000 | grep ihatemoney"); + ''; + } +) [ "sqlite" "postgresql" ] diff --git a/pkgs/development/python-modules/ihatemoney/default.nix b/pkgs/development/python-modules/ihatemoney/default.nix new file mode 100644 index 00000000000000..e37dfe80e580e0 --- /dev/null +++ b/pkgs/development/python-modules/ihatemoney/default.nix @@ -0,0 +1,91 @@ +{ buildPythonPackage, lib, fetchFromGitHub, nixosTests +, alembic +, aniso8601 +, Babel +, blinker +, click +, dnspython +, email_validator +, flask +, flask-babel +, flask-cors +, flask_mail +, flask_migrate +, flask-restful +, flask_script +, flask_sqlalchemy +, flask_wtf +, idna +, itsdangerous +, jinja2 +, Mako +, markupsafe +, python-dateutil +, pytz +, six +, sqlalchemy +, werkzeug +, wtforms +, psycopg2 # optional, for postgresql support +, flask_testing +}: + +buildPythonPackage rec { + pname = "ihatemoney"; + version = "4.1"; + + src = fetchFromGitHub { + owner = "spiral-project"; + repo = pname; + rev = version; + sha256 = "1ai7v2i2rvswzv21nwyq51fvp8lr2x2cl3n34p11br06kc1pcmin"; + }; + + propagatedBuildInputs = [ + alembic + aniso8601 + Babel + blinker + click + dnspython + email_validator + flask + flask-babel + flask-cors + flask_mail + flask_migrate + flask-restful + flask_script + flask_sqlalchemy + flask_wtf + idna + itsdangerous + jinja2 + Mako + markupsafe + python-dateutil + pytz + six + sqlalchemy + werkzeug + wtforms + psycopg2 + ]; + + checkInputs = [ + flask_testing + ]; + + passthru.tests = { + inherit (nixosTests) ihatemoney; + }; + meta = with lib; { + homepage = "https://ihatemoney.org"; + description = "A simple shared budget manager web application"; + platforms = platforms.linux; + license = licenses.beerware; + maintainers = [ maintainers.symphorien ]; + }; +} + + diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index 7cbae0956e79f1..ada5daa000b94a 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -761,6 +761,8 @@ in { i3ipc = callPackage ../development/python-modules/i3ipc { }; + ihatemoney = callPackage ../development/python-modules/ihatemoney { }; + imutils = callPackage ../development/python-modules/imutils { }; inotify-simple = callPackage ../development/python-modules/inotify-simple { };