diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 5214126ff7e..652d7b4472d 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -804,6 +804,7 @@
./services/web-apps/nexus.nix
./services/web-apps/pgpkeyserver-lite.nix
./services/web-apps/matomo.nix
+ ./services/web-apps/moinmoin.nix
./services/web-apps/restya-board.nix
./services/web-apps/tt-rss.nix
./services/web-apps/selfoss.nix
diff --git a/nixos/modules/services/web-apps/moinmoin.nix b/nixos/modules/services/web-apps/moinmoin.nix
new file mode 100644
index 00000000000..0fee64be0bb
--- /dev/null
+++ b/nixos/modules/services/web-apps/moinmoin.nix
@@ -0,0 +1,303 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+ cfg = config.services.moinmoin;
+ python = pkgs.python27;
+ pkg = python.pkgs.moinmoin;
+ dataDir = "/var/lib/moin";
+ usingGunicorn = cfg.webServer == "nginx-gunicorn" || cfg.webServer == "gunicorn";
+ usingNginx = cfg.webServer == "nginx-gunicorn";
+ user = "moin";
+ group = "moin";
+
+ uLit = s: ''u"${s}"'';
+ indentLines = n: str: concatMapStrings (line: "${fixedWidthString n " " " "}${line}\n") (splitString "\n" str);
+
+ moinCliWrapper = wikiIdent: pkgs.writeShellScriptBin "moin-${wikiIdent}" ''
+ ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} -c "${pkg}/bin/moin --config-dir=/var/lib/moin/${wikiIdent}/config $*" ${user}
+ '';
+
+ wikiConfig = wikiIdent: w: ''
+ # -*- coding: utf-8 -*-
+
+ from MoinMoin.config import multiconfig, url_prefix_static
+
+ class Config(multiconfig.DefaultConfig):
+ ${optionalString (w.webLocation != "/") ''
+ url_prefix_static = '${w.webLocation}' + url_prefix_static
+ ''}
+
+ sitename = u'${w.siteName}'
+ page_front_page = u'${w.frontPage}'
+
+ data_dir = '${dataDir}/${wikiIdent}/data'
+ data_underlay_dir = '${dataDir}/${wikiIdent}/underlay'
+
+ language_default = u'${w.languageDefault}'
+ ${optionalString (w.superUsers != []) ''
+ superuser = [${concatMapStringsSep ", " uLit w.superUsers}]
+ ''}
+
+ ${indentLines 4 w.extraConfig}
+ '';
+ wikiConfigFile = name: wiki: pkgs.writeText "${name}.py" (wikiConfig name wiki);
+
+in
+{
+ options.services.moinmoin = with types; {
+ enable = mkEnableOption "MoinMoin Wiki Engine";
+
+ webServer = mkOption {
+ type = enum [ "nginx-gunicorn" "gunicorn" "none" ];
+ default = "nginx-gunicorn";
+ example = "none";
+ description = ''
+ Which web server to use to serve the wiki.
+ Use none if you want to configure this yourself.
+ '';
+ };
+
+ gunicorn.workers = mkOption {
+ type = ints.positive;
+ default = 3;
+ example = 10;
+ description = ''
+ The number of worker processes for handling requests.
+ '';
+ };
+
+ wikis = mkOption {
+ type = attrsOf (submodule ({ name, ... }: {
+ options = {
+ siteName = mkOption {
+ type = str;
+ default = "Untitled Wiki";
+ example = "ExampleWiki";
+ description = ''
+ Short description of your wiki site, displayed below the logo on each page, and
+ used in RSS documents as the channel title.
+ '';
+ };
+
+ webHost = mkOption {
+ type = str;
+ description = "Host part of the wiki URL. If undefined, the name of the attribute set will be used.";
+ example = "wiki.example.org";
+ };
+
+ webLocation = mkOption {
+ type = str;
+ default = "/";
+ example = "/moin";
+ description = "Location part of the wiki URL.";
+ };
+
+ frontPage = mkOption {
+ type = str;
+ default = "LanguageSetup";
+ example = "FrontPage";
+ description = ''
+ Front page name. Set this to something like FrontPage once languages are
+ configured.
+ '';
+ };
+
+ superUsers = mkOption {
+ type = listOf str;
+ default = [];
+ example = [ "elvis" ];
+ description = ''
+ List of trusted user names with wiki system administration super powers.
+
+ Please note that accounts for these users need to be created using the moin command-line utility, e.g.:
+ moin-WIKINAME account create --name=NAME --email=EMAIL --password=PASSWORD.
+ '';
+ };
+
+ languageDefault = mkOption {
+ type = str;
+ default = "en";
+ example = "de";
+ description = "The ISO-639-1 name of the main wiki language. Languages that MoinMoin does not support are ignored.";
+ };
+
+ extraConfig = mkOption {
+ type = lines;
+ default = "";
+ example = ''
+ show_hosts = True
+ search_results_per_page = 100
+ acl_rights_default = u"Known:read,write,delete,revert All:read"
+ logo_string = u"
\U0001f639
"
+ theme_default = u"modernized"
+
+ user_checkbox_defaults = {'show_page_trail': 0, 'edit_on_doubleclick': 0}
+ navi_bar = [u'SomePage'] + multiconfig.DefaultConfig.navi_bar
+ actions_excluded = multiconfig.DefaultConfig.actions_excluded + ['newaccount']
+
+ mail_smarthost = "mail.example.org"
+ mail_from = u"Example.Org Wiki "
+ '';
+ description = ''
+ Additional configuration to be appended verbatim to this wiki's config.
+
+ See for documentation.
+ '';
+ };
+
+ };
+ config = {
+ webHost = mkDefault name;
+ };
+ }));
+ example = literalExample ''
+ {
+ "mywiki" = {
+ siteName = "Example Wiki";
+ webHost = "wiki.example.org";
+ superUsers = [ "admin" ];
+ frontPage = "Index";
+ extraConfig = "page_category_regex = ur'(?P(Category|Kategorie)(?P(?!Template)\S+))'"
+ };
+ }
+ '';
+ description = ''
+ Configurations of the individual wikis. Attribute names must be valid Python
+ identifiers of the form [A-Za-z_][A-Za-z0-9_]*.
+
+ For every attribute WIKINAME, a helper script
+ moin-WIKINAME is created which runs the
+ moin command under the moin user (to avoid
+ file ownership issues) and with the right configuration directory passed to it.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = forEach (attrNames cfg.wikis) (wname:
+ { assertion = builtins.match "[A-Za-z_][A-Za-z0-9_]*" wname != null;
+ message = "${wname} is not valid Python identifier";
+ }
+ );
+
+ users.users = {
+ moin = {
+ description = "MoinMoin wiki";
+ home = dataDir;
+ group = group;
+ isSystemUser = true;
+ };
+ };
+
+ users.groups = {
+ moin = {
+ members = mkIf usingNginx [ config.services.nginx.user ];
+ };
+ };
+
+ environment.systemPackages = [ pkg ] ++ map moinCliWrapper (attrNames cfg.wikis);
+
+ systemd.services = mkIf usingGunicorn
+ (flip mapAttrs' cfg.wikis (wikiIdent: wiki:
+ nameValuePair "moin-${wikiIdent}"
+ {
+ description = "MoinMoin wiki ${wikiIdent} - gunicorn process";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+ restartIfChanged = true;
+ restartTriggers = [ (wikiConfigFile wikiIdent wiki) ];
+
+ environment = let
+ penv = python.buildEnv.override {
+ # setuptools: https://github.com/benoitc/gunicorn/issues/1716
+ extraLibs = [ python.pkgs.gevent python.pkgs.setuptools pkg ];
+ };
+ in {
+ PYTHONPATH = "${dataDir}/${wikiIdent}/config:${penv}/${python.sitePackages}";
+ };
+
+ preStart = ''
+ umask 0007
+ rm -rf ${dataDir}/${wikiIdent}/underlay
+ cp -r ${pkg}/share/moin/underlay ${dataDir}/${wikiIdent}/
+ chmod -R u+w ${dataDir}/${wikiIdent}/underlay
+ '';
+
+ serviceConfig = {
+ User = user;
+ Group = group;
+ WorkingDirectory = "${dataDir}/${wikiIdent}";
+ ExecStart = ''${python.pkgs.gunicorn}/bin/gunicorn moin_wsgi \
+ --name gunicorn-${wikiIdent} \
+ --workers ${toString cfg.gunicorn.workers} \
+ --worker-class gevent \
+ --bind unix:/run/moin/${wikiIdent}/gunicorn.sock
+ '';
+
+ Restart = "on-failure";
+ RestartSec = "2s";
+ StartLimitIntervalSec = "30s";
+
+ StateDirectory = "moin/${wikiIdent}";
+ StateDirectoryMode = "0750";
+ RuntimeDirectory = "moin/${wikiIdent}";
+ RuntimeDirectoryMode = "0750";
+
+ NoNewPrivileges = true;
+ ProtectSystem = "strict";
+ ProtectHome = true;
+ PrivateTmp = true;
+ PrivateDevices = true;
+ PrivateNetwork = true;
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectControlGroups = true;
+ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+ RestrictNamespaces = true;
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ RestrictRealtime = true;
+ };
+ }
+ ));
+
+ services.nginx = mkIf usingNginx {
+ enable = true;
+ virtualHosts = flip mapAttrs' cfg.wikis (name: w: nameValuePair w.webHost {
+ forceSSL = mkDefault true;
+ enableACME = mkDefault true;
+ locations."${w.webLocation}" = {
+ extraConfig = ''
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Server $host;
+
+ proxy_pass http://unix:/run/moin/${name}/gunicorn.sock;
+ '';
+ };
+ });
+ };
+
+ systemd.tmpfiles.rules = [
+ "d /run/moin 0750 ${user} ${group} - -"
+ "d ${dataDir} 0550 ${user} ${group} - -"
+ ]
+ ++ (concatLists (flip mapAttrsToList cfg.wikis (wikiIdent: wiki: [
+ "d ${dataDir}/${wikiIdent} 0750 ${user} ${group} - -"
+ "d ${dataDir}/${wikiIdent}/config 0550 ${user} ${group} - -"
+ "L+ ${dataDir}/${wikiIdent}/config/wikiconfig.py - - - - ${wikiConfigFile wikiIdent wiki}"
+ # needed in order to pass module name to gunicorn
+ "L+ ${dataDir}/${wikiIdent}/config/moin_wsgi.py - - - - ${pkg}/share/moin/server/moin.wsgi"
+ # seed data files
+ "C ${dataDir}/${wikiIdent}/data 0770 ${user} ${group} - ${pkg}/share/moin/data"
+ # fix nix store permissions
+ "Z ${dataDir}/${wikiIdent}/data 0770 ${user} ${group} - -"
+ ])));
+ };
+
+ meta.maintainers = with lib.maintainers; [ b42 ];
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index e94c9712cbf..50a60049a18 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -166,6 +166,7 @@ in
minio = handleTest ./minio.nix {};
minidlna = handleTest ./minidlna.nix {};
misc = handleTest ./misc.nix {};
+ moinmoin = handleTest ./moinmoin.nix {};
mongodb = handleTest ./mongodb.nix {};
moodle = handleTest ./moodle.nix {};
morty = handleTest ./morty.nix {};
diff --git a/nixos/tests/moinmoin.nix b/nixos/tests/moinmoin.nix
new file mode 100644
index 00000000000..2662b79aa09
--- /dev/null
+++ b/nixos/tests/moinmoin.nix
@@ -0,0 +1,24 @@
+import ./make-test.nix ({ pkgs, lib, ... }: {
+ name = "moinmoin";
+ meta.maintainers = [ ]; # waiting for https://github.com/NixOS/nixpkgs/pull/65397
+
+ machine =
+ { ... }:
+ { services.moinmoin.enable = true;
+ services.moinmoin.wikis.ExampleWiki.superUsers = [ "admin" ];
+ services.moinmoin.wikis.ExampleWiki.webHost = "localhost";
+
+ services.nginx.virtualHosts.localhost.enableACME = false;
+ services.nginx.virtualHosts.localhost.forceSSL = false;
+ };
+
+ testScript = ''
+ startAll;
+
+ $machine->waitForUnit('moin-ExampleWiki.service');
+ $machine->waitForUnit('nginx.service');
+ $machine->waitForFile('/run/moin/ExampleWiki/gunicorn.sock');
+ $machine->succeed('curl -L http://localhost/') =~ /If you have just installed/ or die;
+ $machine->succeed('moin-ExampleWiki account create --name=admin --email=admin@example.com --password=foo 2>&1') =~ /status success/ or die;
+ '';
+})