Merge pull request #68973 from symphorien/ihatemoney

ihatemoney: init at 4.1 plus module and test
This commit is contained in:
Léo Gaspard 2020-01-09 04:12:54 +01:00 committed by GitHub
commit 8fcf9926fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 300 additions and 7 deletions

View file

@ -806,6 +806,7 @@
./services/web-apps/gotify-server.nix ./services/web-apps/gotify-server.nix
./services/web-apps/icingaweb2/icingaweb2.nix ./services/web-apps/icingaweb2/icingaweb2.nix
./services/web-apps/icingaweb2/module-monitoring.nix ./services/web-apps/icingaweb2/module-monitoring.nix
./services/web-apps/ihatemoney
./services/web-apps/limesurvey.nix ./services/web-apps/limesurvey.nix
./services/web-apps/mattermost.nix ./services/web-apps/mattermost.nix
./services/web-apps/mediawiki.nix ./services/web-apps/mediawiki.nix

View file

@ -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 <literal>postgresql</literal> is selected, then a database called
<literal>${db}</literal> 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 <literal>ihatemoney generate_password_hash</literal>";
};
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;
};
};
};
}

View file

@ -5,10 +5,6 @@ with lib;
let let
cfg = config.services.uwsgi; cfg = config.services.uwsgi;
uwsgi = pkgs.uwsgi.override {
plugins = cfg.plugins;
};
buildCfg = name: c: buildCfg = name: c:
let let
plugins = plugins =
@ -23,8 +19,8 @@ let
python = python =
if hasPython2 && hasPython3 then if hasPython2 && hasPython3 then
throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3" throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
else if hasPython2 then uwsgi.python2 else if hasPython2 then cfg.package.python2
else if hasPython3 then uwsgi.python3 else if hasPython3 then cfg.package.python3
else null; else null;
pythonEnv = python.withPackages (c.pythonPackages or (self: [])); pythonEnv = python.withPackages (c.pythonPackages or (self: []));
@ -77,6 +73,11 @@ in {
description = "Where uWSGI communication sockets can live"; description = "Where uWSGI communication sockets can live";
}; };
package = mkOption {
type = types.package;
internal = true;
};
instance = mkOption { instance = mkOption {
type = types.attrs; type = types.attrs;
default = { default = {
@ -138,7 +139,7 @@ in {
''; '';
serviceConfig = { serviceConfig = {
Type = "notify"; 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"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
NotifyAccess = "main"; NotifyAccess = "main";
@ -156,5 +157,9 @@ in {
users.groups = optionalAttrs (cfg.group == "uwsgi") { users.groups = optionalAttrs (cfg.group == "uwsgi") {
uwsgi.gid = config.ids.gids.uwsgi; uwsgi.gid = config.ids.gids.uwsgi;
}; };
services.uwsgi.package = pkgs.uwsgi.override {
inherit (cfg) plugins;
};
}; };
} }

View file

@ -122,6 +122,7 @@ in
i3wm = handleTest ./i3wm.nix {}; i3wm = handleTest ./i3wm.nix {};
icingaweb2 = handleTest ./icingaweb2.nix {}; icingaweb2 = handleTest ./icingaweb2.nix {};
iftop = handleTest ./iftop.nix {}; iftop = handleTest ./iftop.nix {};
ihatemoney = handleTest ./ihatemoney.nix {};
incron = handleTest ./incron.nix {}; incron = handleTest ./incron.nix {};
influxdb = handleTest ./influxdb.nix {}; influxdb = handleTest ./influxdb.nix {};
initrd-network-ssh = handleTest ./initrd-network-ssh {}; initrd-network-ssh = handleTest ./initrd-network-ssh {};

View file

@ -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" ]

View file

@ -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 ];
};
}

View file

@ -761,6 +761,8 @@ in {
i3ipc = callPackage ../development/python-modules/i3ipc { }; i3ipc = callPackage ../development/python-modules/i3ipc { };
ihatemoney = callPackage ../development/python-modules/ihatemoney { };
imutils = callPackage ../development/python-modules/imutils { }; imutils = callPackage ../development/python-modules/imutils { };
inotify-simple = callPackage ../development/python-modules/inotify-simple { }; inotify-simple = callPackage ../development/python-modules/inotify-simple { };