at master 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 force_install_noedit = 2; 61 force_install_main_data_root = "${cfg.stateDir}/documents"; 62 force_install_nophpinfo = true; 63 force_install_lockinstall = "444"; 64 force_install_distrib = "nixos"; 65 force_install_type = "mysqli"; 66 force_install_dbserver = cfg.database.host; 67 force_install_port = toString cfg.database.port; 68 force_install_database = cfg.database.name; 69 force_install_databaselogin = cfg.database.user; 70 71 force_install_mainforcehttps = vhostCfg.forceSSL or false; 72 force_install_createuser = false; 73 force_install_dolibarrlogin = null; 74 } 75 // optionalAttrs (cfg.database.passwordFile != null) { 76 force_install_databasepass = ''file_get_contents("${cfg.database.passwordFile}")''; 77 }; 78in 79{ 80 # interface 81 options.services.dolibarr = { 82 enable = mkEnableOption "dolibarr"; 83 84 package = mkPackageOption pkgs "dolibarr" { }; 85 86 domain = mkOption { 87 type = types.str; 88 default = "localhost"; 89 description = '' 90 Domain name of your server. 91 ''; 92 }; 93 94 user = mkOption { 95 type = types.str; 96 default = "dolibarr"; 97 description = '' 98 User account under which dolibarr runs. 99 100 ::: {.note} 101 If left as the default value this user will automatically be created 102 on system activation, otherwise you are responsible for 103 ensuring the user exists before the dolibarr application starts. 104 ::: 105 ''; 106 }; 107 108 group = mkOption { 109 type = types.str; 110 default = "dolibarr"; 111 description = '' 112 Group account under which dolibarr runs. 113 114 ::: {.note} 115 If left as the default value this group will automatically be created 116 on system activation, otherwise you are responsible for 117 ensuring the group exists before the dolibarr application starts. 118 ::: 119 ''; 120 }; 121 122 stateDir = mkOption { 123 type = types.str; 124 default = "/var/lib/dolibarr"; 125 description = '' 126 State and configuration directory dolibarr will use. 127 ''; 128 }; 129 130 database = { 131 host = mkOption { 132 type = types.str; 133 default = "localhost"; 134 description = "Database host address."; 135 }; 136 port = mkOption { 137 type = types.port; 138 default = 3306; 139 description = "Database host port."; 140 }; 141 name = mkOption { 142 type = types.str; 143 default = "dolibarr"; 144 description = "Database name."; 145 }; 146 user = mkOption { 147 type = types.str; 148 default = "dolibarr"; 149 description = "Database username."; 150 }; 151 passwordFile = mkOption { 152 type = with types; nullOr path; 153 default = null; 154 example = "/run/keys/dolibarr-dbpassword"; 155 description = "Database password file."; 156 }; 157 createLocally = mkOption { 158 type = types.bool; 159 default = true; 160 description = "Create the database and database user locally."; 161 }; 162 }; 163 164 settings = mkOption { 165 type = 166 with types; 167 (attrsOf (oneOf [ 168 bool 169 int 170 str 171 ])); 172 default = { }; 173 description = "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details."; 174 }; 175 176 nginx = mkOption { 177 type = types.nullOr ( 178 types.submodule ( 179 lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { 180 # enable encryption by default, 181 # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text. 182 options.forceSSL.default = true; 183 options.enableACME.default = true; 184 } 185 ) 186 ); 187 default = null; 188 example = lib.literalExpression '' 189 { 190 serverAliases = [ 191 "dolibarr.''${config.networking.domain}" 192 "erp.''${config.networking.domain}" 193 ]; 194 enableACME = false; 195 } 196 ''; 197 description = '' 198 With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr. 199 Set to {} if you do not need any customization to the virtual host. 200 If enabled, then by default, the {option}`serverName` is 201 `''${domain}`, 202 SSL is active, and certificates are acquired via ACME. 203 If this is set to null (the default), no nginx virtualHost will be configured. 204 ''; 205 }; 206 207 poolConfig = mkOption { 208 type = 209 with types; 210 attrsOf (oneOf [ 211 str 212 int 213 bool 214 ]); 215 default = { 216 "pm" = "dynamic"; 217 "pm.max_children" = 32; 218 "pm.start_servers" = 2; 219 "pm.min_spare_servers" = 2; 220 "pm.max_spare_servers" = 4; 221 "pm.max_requests" = 500; 222 }; 223 description = '' 224 Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php) 225 for details on configuration directives. 226 ''; 227 }; 228 }; 229 230 # implementation 231 config = mkIf cfg.enable (mkMerge [ 232 { 233 234 assertions = [ 235 { 236 assertion = cfg.database.createLocally -> cfg.database.user == cfg.user; 237 message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned"; 238 } 239 ]; 240 241 services.dolibarr.settings = { 242 dolibarr_main_url_root = "https://${cfg.domain}"; 243 dolibarr_main_document_root = "${package}/htdocs"; 244 dolibarr_main_url_root_alt = "/custom"; 245 dolibarr_main_data_root = "${cfg.stateDir}/documents"; 246 247 dolibarr_main_db_host = cfg.database.host; 248 dolibarr_main_db_port = toString cfg.database.port; 249 dolibarr_main_db_name = cfg.database.name; 250 dolibarr_main_db_prefix = "llx_"; 251 dolibarr_main_db_user = cfg.database.user; 252 dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) '' 253 file_get_contents("${cfg.database.passwordFile}") 254 ''; 255 dolibarr_main_db_type = "mysqli"; 256 dolibarr_main_db_character_set = mkDefault "utf8"; 257 dolibarr_main_db_collation = mkDefault "utf8_unicode_ci"; 258 259 # Authentication settings 260 dolibarr_main_authentication = mkDefault "dolibarr"; 261 262 # Security settings 263 dolibarr_main_prod = true; 264 dolibarr_main_force_https = vhostCfg.forceSSL or false; 265 dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql"; 266 dolibarr_nocsrfcheck = false; 267 dolibarr_main_instance_unique_id = '' 268 file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id") 269 ''; 270 dolibarr_mailing_limit_sendbyweb = false; 271 }; 272 273 systemd.tmpfiles.rules = [ 274 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}" 275 "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}" 276 "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}" 277 "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}" 278 ]; 279 280 services.mysql = mkIf cfg.database.createLocally { 281 enable = mkDefault true; 282 package = mkDefault pkgs.mariadb; 283 ensureDatabases = [ cfg.database.name ]; 284 ensureUsers = [ 285 { 286 name = cfg.database.user; 287 ensurePermissions = { 288 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 289 }; 290 } 291 ]; 292 }; 293 294 services.nginx.enable = mkIf (cfg.nginx != null) true; 295 services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) ( 296 lib.mkMerge [ 297 cfg.nginx 298 ({ 299 root = lib.mkForce "${package}/htdocs"; 300 locations."/".index = "index.php"; 301 locations."~ [^/]\\.php(/|$)" = { 302 extraConfig = '' 303 fastcgi_split_path_info ^(.+?\.php)(/.*)$; 304 fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket}; 305 ''; 306 }; 307 }) 308 ] 309 ); 310 311 systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ]; 312 services.phpfpm.pools.dolibarr = { 313 inherit (cfg) user group; 314 phpPackage = pkgs.php83.buildEnv { 315 extensions = { enabled, all }: enabled ++ [ all.calendar ]; 316 # recommended by dolibarr web application 317 extraConfig = '' 318 session.use_strict_mode = 1 319 session.cookie_samesite = "Lax" 320 ; open_basedir = "${package}/htdocs, ${cfg.stateDir}" 321 allow_url_fopen = 0 322 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" 323 ''; 324 }; 325 326 settings = { 327 "listen.mode" = "0660"; 328 "listen.owner" = cfg.user; 329 "listen.group" = cfg.group; 330 } 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}