From 32d2266d0df1eeddd28f4d52fc7c04d734deb2f5 Mon Sep 17 00:00:00 2001 From: Symphorien Gibol Date: Mon, 16 Sep 2019 17:08:32 +0200 Subject: [PATCH 1/4] ihatemoney: init at 4.1 plus module and test --- nixos/modules/module-list.nix | 1 + .../services/web-apps/ihatemoney/default.nix | 141 ++++++++++++++++++ nixos/modules/services/web-servers/uwsgi.nix | 19 ++- nixos/tests/all-tests.nix | 1 + nixos/tests/ihatemoney.nix | 49 ++++++ .../python-modules/ihatemoney/default.nix | 91 +++++++++++ pkgs/top-level/python-packages.nix | 2 + 7 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 nixos/modules/services/web-apps/ihatemoney/default.nix create mode 100644 nixos/tests/ihatemoney.nix create mode 100644 pkgs/development/python-modules/ihatemoney/default.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5b7f391ed5a50d..16d4b733fdb8ec 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -793,6 +793,7 @@ ./services/web-apps/frab.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 af70f32f32d0f9..66537b29cd21d7 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 { { name = "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 60762de76d338a..a6b8f90fc9aa39 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -121,6 +121,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..a2f644e512b7b6 --- /dev/null +++ b/nixos/tests/ihatemoney.nix @@ -0,0 +1,49 @@ +{ 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 owership 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 persitent + 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 6e936afd5ea171..f923b64482eca2 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -671,6 +671,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 { }; From 54b0ab0ac63702b9bd7882eb53fd4b202df64949 Mon Sep 17 00:00:00 2001 From: symphorien Date: Sun, 5 Jan 2020 22:04:32 +0000 Subject: [PATCH 2/4] Update nixos/tests/ihatemoney.nix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Léo Gaspard --- nixos/tests/ihatemoney.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix index a2f644e512b7b6..802aa7362a9160 100644 --- a/nixos/tests/ihatemoney.nix +++ b/nixos/tests/ihatemoney.nix @@ -34,7 +34,7 @@ in map (backend: makeTest { 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 owership for the secret key: $owner, is uwsgi running as the right user ?" unless $owner eq "ihatemoney:ihatemoney"; + 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); From b8b9e7be6dc1e5e7c45040cf0702be0db42121c8 Mon Sep 17 00:00:00 2001 From: symphorien Date: Sun, 5 Jan 2020 22:04:42 +0000 Subject: [PATCH 3/4] Update nixos/tests/ihatemoney.nix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Léo Gaspard --- nixos/tests/ihatemoney.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix index 802aa7362a9160..31e85aceccf273 100644 --- a/nixos/tests/ihatemoney.nix +++ b/nixos/tests/ihatemoney.nix @@ -39,7 +39,7 @@ in map (backend: makeTest { $machine->start(); $machine->waitForOpenPort(8000); $machine->waitForUnit("uwsgi.service"); - # check that the database is really persitent + # 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"); From 665fee312a03aac0db9a61cc302a51626288c7c2 Mon Sep 17 00:00:00 2001 From: Symphorien Gibol Date: Sun, 5 Jan 2020 12:00:00 +0000 Subject: [PATCH 4/4] nixos/tests/ihatemoney.nix: run nixpkgs-fmt now indentation is perfect --- nixos/tests/ihatemoney.nix | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/nixos/tests/ihatemoney.nix b/nixos/tests/ihatemoney.nix index 31e85aceccf273..14db17fe5e6768 100644 --- a/nixos/tests/ihatemoney.nix +++ b/nixos/tests/ihatemoney.nix @@ -1,22 +1,24 @@ -{ system ? builtins.currentSystem, -config ? {}, -pkgs ? import ../.. { inherit system config; } +{ 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"; - }; +in +map ( + backend: makeTest { + name = "ihatemoney-${backend}"; + machine = { lib, ... }: { + services.ihatemoney = { + enable = true; + enablePublicProjectCreation = true; + inherit backend; + uwsgiConfig = { + http = ":8000"; }; - boot.cleanTmpDir = true; + }; + boot.cleanTmpDir = true; # ihatemoney needs a local smtp server otherwise project creation just crashes services.opensmtpd = { enable = true; @@ -46,4 +48,5 @@ in map (backend: makeTest { die unless $timestamp eq $timestamp2; $machine->succeed("curl http://localhost:8000 | grep ihatemoney"); ''; - }) [ "sqlite" "postgresql" ] + } +) [ "sqlite" "postgresql" ]