at 25.11-pre 13 kB view raw
1{ 2 config, 3 pkgs, 4 lib, 5 ... 6}: 7let 8 inherit (lib) 9 any 10 boolToString 11 concatStringsSep 12 isBool 13 isString 14 mapAttrsToList 15 mkDefault 16 mkEnableOption 17 mkIf 18 mkMerge 19 mkOption 20 optionalAttrs 21 types 22 mkPackageOption 23 ; 24 25 package = cfg.package.override { inherit (cfg) stateDir; }; 26 27 cfg = config.services.dolibarr; 28 vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}"; 29 30 mkConfigFile = 31 filename: settings: 32 let 33 # hack in special logic for secrets so we read them from a separate file avoiding the nix store 34 secretKeys = [ 35 "force_install_databasepass" 36 "dolibarr_main_db_pass" 37 "dolibarr_main_instance_unique_id" 38 ]; 39 40 toStr = 41 k: v: 42 if (any (str: k == str) secretKeys) then 43 v 44 else if isString v then 45 "'${v}'" 46 else if isBool v then 47 boolToString v 48 else if v == null then 49 "null" 50 else 51 toString v; 52 in 53 pkgs.writeText filename '' 54 <?php 55 ${concatStringsSep "\n" (mapAttrsToList (k: v: "\$${k} = ${toStr k v};") settings)} 56 ''; 57 58 # see https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/install/install.forced.sample.php for all possible values 59 install = 60 { 61 force_install_noedit = 2; 62 force_install_main_data_root = "${cfg.stateDir}/documents"; 63 force_install_nophpinfo = true; 64 force_install_lockinstall = "444"; 65 force_install_distrib = "nixos"; 66 force_install_type = "mysqli"; 67 force_install_dbserver = cfg.database.host; 68 force_install_port = toString cfg.database.port; 69 force_install_database = cfg.database.name; 70 force_install_databaselogin = cfg.database.user; 71 72 force_install_mainforcehttps = vhostCfg.forceSSL or false; 73 force_install_createuser = false; 74 force_install_dolibarrlogin = null; 75 } 76 // optionalAttrs (cfg.database.passwordFile != null) { 77 force_install_databasepass = ''file_get_contents("${cfg.database.passwordFile}")''; 78 }; 79in 80{ 81 # interface 82 options.services.dolibarr = { 83 enable = mkEnableOption "dolibarr"; 84 85 package = mkPackageOption pkgs "dolibarr" { }; 86 87 domain = mkOption { 88 type = types.str; 89 default = "localhost"; 90 description = '' 91 Domain name of your server. 92 ''; 93 }; 94 95 user = mkOption { 96 type = types.str; 97 default = "dolibarr"; 98 description = '' 99 User account under which dolibarr runs. 100 101 ::: {.note} 102 If left as the default value this user will automatically be created 103 on system activation, otherwise you are responsible for 104 ensuring the user exists before the dolibarr application starts. 105 ::: 106 ''; 107 }; 108 109 group = mkOption { 110 type = types.str; 111 default = "dolibarr"; 112 description = '' 113 Group account under which dolibarr runs. 114 115 ::: {.note} 116 If left as the default value this group will automatically be created 117 on system activation, otherwise you are responsible for 118 ensuring the group exists before the dolibarr application starts. 119 ::: 120 ''; 121 }; 122 123 stateDir = mkOption { 124 type = types.str; 125 default = "/var/lib/dolibarr"; 126 description = '' 127 State and configuration directory dolibarr will use. 128 ''; 129 }; 130 131 database = { 132 host = mkOption { 133 type = types.str; 134 default = "localhost"; 135 description = "Database host address."; 136 }; 137 port = mkOption { 138 type = types.port; 139 default = 3306; 140 description = "Database host port."; 141 }; 142 name = mkOption { 143 type = types.str; 144 default = "dolibarr"; 145 description = "Database name."; 146 }; 147 user = mkOption { 148 type = types.str; 149 default = "dolibarr"; 150 description = "Database username."; 151 }; 152 passwordFile = mkOption { 153 type = with types; nullOr path; 154 default = null; 155 example = "/run/keys/dolibarr-dbpassword"; 156 description = "Database password file."; 157 }; 158 createLocally = mkOption { 159 type = types.bool; 160 default = true; 161 description = "Create the database and database user locally."; 162 }; 163 }; 164 165 settings = mkOption { 166 type = 167 with types; 168 (attrsOf (oneOf [ 169 bool 170 int 171 str 172 ])); 173 default = { }; 174 description = "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details."; 175 }; 176 177 nginx = mkOption { 178 type = types.nullOr ( 179 types.submodule ( 180 lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { 181 # enable encryption by default, 182 # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text. 183 options.forceSSL.default = true; 184 options.enableACME.default = true; 185 } 186 ) 187 ); 188 default = null; 189 example = lib.literalExpression '' 190 { 191 serverAliases = [ 192 "dolibarr.''${config.networking.domain}" 193 "erp.''${config.networking.domain}" 194 ]; 195 enableACME = false; 196 } 197 ''; 198 description = '' 199 With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr. 200 Set to {} if you do not need any customization to the virtual host. 201 If enabled, then by default, the {option}`serverName` is 202 `''${domain}`, 203 SSL is active, and certificates are acquired via ACME. 204 If this is set to null (the default), no nginx virtualHost will be configured. 205 ''; 206 }; 207 208 poolConfig = mkOption { 209 type = 210 with types; 211 attrsOf (oneOf [ 212 str 213 int 214 bool 215 ]); 216 default = { 217 "pm" = "dynamic"; 218 "pm.max_children" = 32; 219 "pm.start_servers" = 2; 220 "pm.min_spare_servers" = 2; 221 "pm.max_spare_servers" = 4; 222 "pm.max_requests" = 500; 223 }; 224 description = '' 225 Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php) 226 for details on configuration directives. 227 ''; 228 }; 229 }; 230 231 # implementation 232 config = mkIf cfg.enable (mkMerge [ 233 { 234 235 assertions = [ 236 { 237 assertion = cfg.database.createLocally -> cfg.database.user == cfg.user; 238 message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned"; 239 } 240 ]; 241 242 services.dolibarr.settings = { 243 dolibarr_main_url_root = "https://${cfg.domain}"; 244 dolibarr_main_document_root = "${package}/htdocs"; 245 dolibarr_main_url_root_alt = "/custom"; 246 dolibarr_main_data_root = "${cfg.stateDir}/documents"; 247 248 dolibarr_main_db_host = cfg.database.host; 249 dolibarr_main_db_port = toString cfg.database.port; 250 dolibarr_main_db_name = cfg.database.name; 251 dolibarr_main_db_prefix = "llx_"; 252 dolibarr_main_db_user = cfg.database.user; 253 dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) '' 254 file_get_contents("${cfg.database.passwordFile}") 255 ''; 256 dolibarr_main_db_type = "mysqli"; 257 dolibarr_main_db_character_set = mkDefault "utf8"; 258 dolibarr_main_db_collation = mkDefault "utf8_unicode_ci"; 259 260 # Authentication settings 261 dolibarr_main_authentication = mkDefault "dolibarr"; 262 263 # Security settings 264 dolibarr_main_prod = true; 265 dolibarr_main_force_https = vhostCfg.forceSSL or false; 266 dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql"; 267 dolibarr_nocsrfcheck = false; 268 dolibarr_main_instance_unique_id = '' 269 file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id") 270 ''; 271 dolibarr_mailing_limit_sendbyweb = false; 272 }; 273 274 systemd.tmpfiles.rules = [ 275 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}" 276 "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}" 277 "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}" 278 "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}" 279 ]; 280 281 services.mysql = mkIf cfg.database.createLocally { 282 enable = mkDefault true; 283 package = mkDefault pkgs.mariadb; 284 ensureDatabases = [ cfg.database.name ]; 285 ensureUsers = [ 286 { 287 name = cfg.database.user; 288 ensurePermissions = { 289 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 290 }; 291 } 292 ]; 293 }; 294 295 services.nginx.enable = mkIf (cfg.nginx != null) true; 296 services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) ( 297 lib.mkMerge [ 298 cfg.nginx 299 ({ 300 root = lib.mkForce "${package}/htdocs"; 301 locations."/".index = "index.php"; 302 locations."~ [^/]\\.php(/|$)" = { 303 extraConfig = '' 304 fastcgi_split_path_info ^(.+?\.php)(/.*)$; 305 fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket}; 306 ''; 307 }; 308 }) 309 ] 310 ); 311 312 systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ]; 313 services.phpfpm.pools.dolibarr = { 314 inherit (cfg) user group; 315 phpPackage = pkgs.php83.buildEnv { 316 extensions = { enabled, all }: enabled ++ [ all.calendar ]; 317 # recommended by dolibarr web application 318 extraConfig = '' 319 session.use_strict_mode = 1 320 session.cookie_samesite = "Lax" 321 ; open_basedir = "${package}/htdocs, ${cfg.stateDir}" 322 allow_url_fopen = 0 323 disable_functions = "pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals" 324 ''; 325 }; 326 327 settings = { 328 "listen.mode" = "0660"; 329 "listen.owner" = cfg.user; 330 "listen.group" = cfg.group; 331 } // cfg.poolConfig; 332 }; 333 334 # there are several challenges with dolibarr and NixOS which we can address here 335 # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php 336 # - the dolibarr installer requires write access to its config file during installation, though not afterwards 337 # - the dolibarr config file generally holds secrets generated by the installer, though the config file is a php file so we can read and write these secrets from an external file 338 systemd.services.dolibarr-config = { 339 description = "dolibarr configuration file management via NixOS"; 340 wantedBy = [ "multi-user.target" ]; 341 342 script = 343 let 344 php = lib.getExe config.services.phpfpm.pools.dolibarr.phpPackage; 345 in 346 '' 347 # extract the 'main instance unique id' secret that the dolibarr installer generated for us, store it in a file for use by our own NixOS generated configuration file 348 ${php} -r "include '${cfg.stateDir}/conf.php'; file_put_contents('${cfg.stateDir}/dolibarr_main_instance_unique_id', \$dolibarr_main_instance_unique_id);" 349 350 # replace configuration file generated by installer with the NixOS generated configuration file 351 install -m 440 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php' 352 ''; 353 354 serviceConfig = { 355 Type = "oneshot"; 356 User = cfg.user; 357 Group = cfg.group; 358 RemainAfterExit = "yes"; 359 }; 360 361 unitConfig = { 362 ConditionFileNotEmpty = "${cfg.stateDir}/conf.php"; 363 }; 364 }; 365 366 users.users.dolibarr = mkIf (cfg.user == "dolibarr") { 367 isSystemUser = true; 368 group = cfg.group; 369 }; 370 371 users.groups = optionalAttrs (cfg.group == "dolibarr") { 372 dolibarr = { }; 373 }; 374 } 375 (mkIf (cfg.nginx != null) { 376 users.users."${config.services.nginx.group}".extraGroups = mkIf (cfg.nginx != null) [ cfg.group ]; 377 }) 378 ]); 379}