diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 388f4788b59..9da9e0970ff 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -790,6 +790,7 @@ ./services/web-apps/mattermost.nix ./services/web-apps/mediawiki.nix ./services/web-apps/miniflux.nix + ./services/web-apps/moodle.nix ./services/web-apps/nextcloud.nix ./services/web-apps/nexus.nix ./services/web-apps/pgpkeyserver-lite.nix diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix new file mode 100644 index 00000000000..f2516c67c6b --- /dev/null +++ b/nixos/modules/services/web-apps/moodle.nix @@ -0,0 +1,300 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types; + inherit (lib) concatStringsSep literalExample mapAttrsToList optional optionalString; + + cfg = config.services.moodle; + fpm = config.services.phpfpm.pools.moodle; + + user = "moodle"; + group = config.services.httpd.group; + stateDir = "/var/lib/moodle"; + + moodleConfig = pkgs.writeText "config.php" '' + dbtype = '${ { "mysql" = "mariadb"; "pgsql" = "pgsql"; }.${cfg.database.type} }'; + $CFG->dblibrary = 'native'; + $CFG->dbhost = '${cfg.database.host}'; + $CFG->dbname = '${cfg.database.name}'; + $CFG->dbuser = '${cfg.database.user}'; + ${optionalString (cfg.database.passwordFile != null) "$CFG->dbpass = file_get_contents('${cfg.database.passwordFile}');"} + $CFG->prefix = 'mdl_'; + $CFG->dboptions = array ( + 'dbpersist' => 0, + 'dbport' => '${toString cfg.database.port}', + ${optionalString (cfg.database.socket != null) "'dbsocket' => '${cfg.database.socket}',"} + 'dbcollation' => 'utf8mb4_unicode_ci', + ); + + $CFG->wwwroot = '${if cfg.virtualHost.enableSSL then "https" else "http"}://${cfg.virtualHost.hostName}'; + $CFG->dataroot = '${stateDir}'; + $CFG->admin = 'admin'; + + $CFG->directorypermissions = 02777; + $CFG->disableupdateautodeploy = true; + + $CFG->pathtogs = '${pkgs.ghostscript}/bin/gs'; + $CFG->pathtophp = '${pkgs.php}/bin/php'; + $CFG->pathtodu = '${pkgs.coreutils}/bin/du'; + $CFG->aspellpath = '${pkgs.aspell}/bin/aspell'; + $CFG->pathtodot = '${pkgs.graphviz}/bin/dot'; + + require_once('${cfg.package}/share/moodle/lib/setup.php'); + + // There is no php closing tag in this file, + // it is intentional because it prevents trailing whitespace problems! + ''; + + mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; + pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; +in +{ + # interface + options.services.moodle = { + enable = mkEnableOption "Moodle web application"; + + package = mkOption { + type = types.package; + default = pkgs.moodle; + defaultText = "pkgs.moodle"; + description = "The Moodle package to use."; + }; + + initialPassword = mkOption { + type = types.str; + example = "correcthorsebatterystaple"; + description = '' + Specifies the initial password for the admin, i.e. the password assigned if the user does not already exist. + The password specified here is world-readable in the Nix store, so it should be changed promptly. + ''; + }; + + database = { + type = mkOption { + type = types.enum [ "mysql" "pgsql" ]; + default = "mysql"; + description = ''Database engine to use.''; + }; + + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + + port = mkOption { + type = types.int; + description = "Database host port."; + default = { + "mysql" = 3306; + "pgsql" = 5432; + }.${cfg.database.type}; + defaultText = "3306"; + }; + + name = mkOption { + type = types.str; + default = "moodle"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "moodle"; + description = "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/moodle-dbpassword"; + description = '' + A file containing the password corresponding to + . + ''; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = + if mysqlLocal then "/run/mysqld/mysqld.sock" + else if pgsqlLocal then "/run/postgresql" + else null; + defaultText = "/run/mysqld/mysqld.sock"; + description = "Path to the unix socket file to use for authentication."; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "Create the database and database user locally."; + }; + }; + + virtualHost = mkOption { + type = types.submodule ({ + options = import ../web-servers/apache-httpd/per-server-options.nix { + inherit lib; + forMainServer = false; + }; + }); + example = { + hostName = "moodle.example.org"; + enableSSL = true; + adminAddr = "webmaster@example.org"; + sslServerCert = "/var/lib/acme/moodle.example.org/full.pem"; + sslServerKey = "/var/lib/acme/moodle.example.org/key.pem"; + }; + description = '' + Apache configuration can be done by adapting . + See for further information. + ''; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = '' + Options for the Moodle PHP pool. See the documentation on php-fpm.conf + for details on configuration directives. + ''; + }; + }; + + # implementation + config = mkIf cfg.enable { + + assertions = [ + { assertion = cfg.database.createLocally -> cfg.database.user == user; + message = "services.moodle.database.user must be set to ${user} if services.moodle.database.createLocally is set true"; + } + { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; + message = "a password cannot be specified if services.moodle.database.createLocally is set to true"; + } + ]; + + services.mysql = mkIf mysqlLocal { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensurePermissions = { + "${cfg.database.name}.*" = "SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER"; + }; + } + ]; + }; + + services.postgresql = mkIf pgsqlLocal { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; + } + ]; + }; + + services.phpfpm.pools.moodle = { + inherit user group; + phpEnv.MOODLE_CONFIG = "${moodleConfig}"; + phpOptions = '' + zend_extension = opcache.so + opcache.enable = 1 + ''; + settings = { + "listen.owner" = config.services.httpd.user; + "listen.group" = config.services.httpd.group; + } // cfg.poolConfig; + }; + + services.httpd = { + enable = true; + adminAddr = mkDefault cfg.virtualHost.adminAddr; + extraModules = [ "proxy_fcgi" ]; + virtualHosts = [ (mkMerge [ + cfg.virtualHost { + documentRoot = mkForce "${cfg.package}/share/moodle"; + extraConfig = '' + + + + SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/" + + + Options -Indexes + DirectoryIndex index.php + + ''; + } + ]) ]; + }; + + systemd.tmpfiles.rules = [ + "d '${stateDir}' 0750 ${user} ${group} - -" + ]; + + systemd.services.moodle-init = { + wantedBy = [ "multi-user.target" ]; + before = [ "phpfpm-moodle.service" ]; + after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; + environment.MOODLE_CONFIG = moodleConfig; + script = '' + ${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/check_database_schema.php && rc=$? || rc=$? + + [ "$rc" == 1 ] && ${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/upgrade.php \ + --non-interactive \ + --allow-unstable + + [ "$rc" == 2 ] && ${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/install_database.php \ + --agree-license \ + --adminpass=${cfg.initialPassword} + + true + ''; + serviceConfig = { + User = user; + Group = group; + Type = "oneshot"; + }; + }; + + systemd.services.moodle-cron = { + description = "Moodle cron service"; + after = [ "moodle-init.service" ]; + environment.MOODLE_CONFIG = moodleConfig; + serviceConfig = { + User = user; + Group = group; + ExecStart = "${pkgs.php}/bin/php ${cfg.package}/share/moodle/admin/cli/cron.php"; + }; + }; + + systemd.timers.moodle-cron = { + description = "Moodle cron timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "minutely"; + }; + }; + + systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; + + users.users."${user}".group = group; + + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 36a053e8e6b..e3e4ddab72c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -161,6 +161,7 @@ in minio = handleTest ./minio.nix {}; misc = handleTest ./misc.nix {}; mongodb = handleTest ./mongodb.nix {}; + moodle = handleTest ./moodle.nix {}; morty = handleTest ./morty.nix {}; mosquitto = handleTest ./mosquitto.nix {}; mpd = handleTest ./mpd.nix {}; diff --git a/nixos/tests/moodle.nix b/nixos/tests/moodle.nix new file mode 100644 index 00000000000..565a6b63694 --- /dev/null +++ b/nixos/tests/moodle.nix @@ -0,0 +1,22 @@ +import ./make-test.nix ({ pkgs, lib, ... }: { + name = "moodle"; + meta.maintainers = [ lib.maintainers.aanderse ]; + + machine = + { ... }: + { services.moodle.enable = true; + services.moodle.virtualHost.hostName = "localhost"; + services.moodle.virtualHost.adminAddr = "root@example.com"; + services.moodle.initialPassword = "correcthorsebatterystaple"; + + # Ensure the virtual machine has enough memory to avoid errors like: + # Fatal error: Out of memory (allocated 152047616) (tried to allocate 33554440 bytes) + virtualisation.memorySize = 2000; + }; + + testScript = '' + startAll; + $machine->waitForUnit('phpfpm-moodle.service'); + $machine->succeed('curl http://localhost/') =~ /You are not logged in/ or die; + ''; +})