at 25.11-pre 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.zammad; 10 settingsFormat = pkgs.formats.yaml { }; 11 filterNull = lib.filterAttrs (_: v: v != null); 12 serviceConfig = { 13 Type = "simple"; 14 Restart = "always"; 15 16 User = cfg.user; 17 Group = cfg.group; 18 PrivateTmp = true; 19 StateDirectory = "zammad"; 20 WorkingDirectory = package; 21 }; 22 environment = { 23 RAILS_ENV = "production"; 24 NODE_ENV = "production"; 25 RAILS_SERVE_STATIC_FILES = "true"; 26 RAILS_LOG_TO_STDOUT = "true"; 27 REDIS_URL = "redis://${cfg.redis.host}:${toString cfg.redis.port}"; 28 }; 29 databaseConfig = settingsFormat.generate "database.yml" cfg.database.settings; 30 package = cfg.package.override { 31 dataDir = cfg.dataDir; 32 }; 33in 34{ 35 36 options = { 37 services.zammad = { 38 enable = lib.mkEnableOption "Zammad, a web-based, open source user support/ticketing solution"; 39 40 package = lib.mkPackageOption pkgs "zammad" { }; 41 42 user = lib.mkOption { 43 type = lib.types.str; 44 default = "zammad"; 45 description = '' 46 Name of the Zammad user. 47 ''; 48 }; 49 50 group = lib.mkOption { 51 type = lib.types.str; 52 default = "zammad"; 53 description = '' 54 Name of the Zammad group. 55 ''; 56 }; 57 58 dataDir = lib.mkOption { 59 type = lib.types.path; 60 default = "/var/lib/zammad"; 61 description = '' 62 Path to a folder that will contain Zammad working directory. 63 ''; 64 }; 65 66 host = lib.mkOption { 67 type = lib.types.str; 68 default = "127.0.0.1"; 69 example = "192.168.23.42"; 70 description = "Host address."; 71 }; 72 73 openPorts = lib.mkOption { 74 type = lib.types.bool; 75 default = false; 76 description = "Whether to open firewall ports for Zammad"; 77 }; 78 79 port = lib.mkOption { 80 type = lib.types.port; 81 default = 3000; 82 description = "Web service port."; 83 }; 84 85 websocketPort = lib.mkOption { 86 type = lib.types.port; 87 default = 6042; 88 description = "Websocket service port."; 89 }; 90 91 redis = { 92 createLocally = lib.mkOption { 93 type = lib.types.bool; 94 default = true; 95 description = "Whether to create a local redis automatically."; 96 }; 97 98 name = lib.mkOption { 99 type = lib.types.str; 100 default = "zammad"; 101 description = '' 102 Name of the redis server. Only used if `createLocally` is set to true. 103 ''; 104 }; 105 106 host = lib.mkOption { 107 type = lib.types.str; 108 default = "localhost"; 109 description = '' 110 Redis server address. 111 ''; 112 }; 113 114 port = lib.mkOption { 115 type = lib.types.port; 116 default = 6379; 117 description = "Port of the redis server."; 118 }; 119 }; 120 121 database = { 122 host = lib.mkOption { 123 type = lib.types.str; 124 default = "/run/postgresql"; 125 description = '' 126 Database host address. 127 ''; 128 }; 129 130 port = lib.mkOption { 131 type = lib.types.nullOr lib.types.port; 132 default = null; 133 description = "Database port. Use `null` for default port."; 134 }; 135 136 name = lib.mkOption { 137 type = lib.types.str; 138 default = "zammad"; 139 description = '' 140 Database name. 141 ''; 142 }; 143 144 user = lib.mkOption { 145 type = lib.types.nullOr lib.types.str; 146 default = "zammad"; 147 description = "Database user."; 148 }; 149 150 passwordFile = lib.mkOption { 151 type = lib.types.nullOr lib.types.path; 152 default = null; 153 example = "/run/keys/zammad-dbpassword"; 154 description = '' 155 A file containing the password for {option}`services.zammad.database.user`. 156 ''; 157 }; 158 159 createLocally = lib.mkOption { 160 type = lib.types.bool; 161 default = true; 162 description = "Whether to create a local database automatically."; 163 }; 164 165 settings = lib.mkOption { 166 type = settingsFormat.type; 167 default = { }; 168 example = lib.literalExpression '' 169 { 170 } 171 ''; 172 description = '' 173 The {file}`database.yml` configuration file as key value set. 174 See \<TODO\> 175 for list of configuration parameters. 176 ''; 177 }; 178 }; 179 180 secretKeyBaseFile = lib.mkOption { 181 type = lib.types.nullOr lib.types.path; 182 default = null; 183 example = "/run/keys/secret_key_base"; 184 description = '' 185 The path to a file containing the 186 `secret_key_base` secret. 187 188 Zammad uses `secret_key_base` to encrypt 189 the cookie store, which contains session data, and to digest 190 user auth tokens. 191 192 Needs to be a 64 byte long string of hexadecimal 193 characters. You can generate one by running 194 195 ``` 196 openssl rand -hex 64 >/path/to/secret_key_base_file 197 ``` 198 199 This should be a string, not a nix path, since nix paths are 200 copied into the world-readable nix store. 201 ''; 202 }; 203 }; 204 }; 205 206 config = lib.mkIf cfg.enable { 207 services.zammad.database.settings = { 208 production = lib.mapAttrs (_: v: lib.mkDefault v) (filterNull { 209 adapter = "postgresql"; 210 database = cfg.database.name; 211 pool = 50; 212 timeout = 5000; 213 encoding = "utf8"; 214 username = cfg.database.user; 215 host = cfg.database.host; 216 port = cfg.database.port; 217 }); 218 }; 219 220 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openPorts [ 221 config.services.zammad.port 222 config.services.zammad.websocketPort 223 ]; 224 225 users.users.${cfg.user} = { 226 group = "${cfg.group}"; 227 isSystemUser = true; 228 }; 229 230 users.groups.${cfg.group} = { }; 231 232 assertions = [ 233 { 234 assertion = 235 cfg.database.createLocally -> cfg.database.user == "zammad" && cfg.database.name == "zammad"; 236 message = "services.zammad.database.user must be set to \"zammad\" if services.zammad.database.createLocally is set to true"; 237 } 238 { 239 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; 240 message = "a password cannot be specified if services.zammad.database.createLocally is set to true"; 241 } 242 { 243 assertion = cfg.redis.createLocally -> cfg.redis.host == "localhost"; 244 message = "the redis host must be localhost if services.zammad.redis.createLocally is set to true"; 245 } 246 ]; 247 248 services.postgresql = lib.optionalAttrs (cfg.database.createLocally) { 249 enable = true; 250 ensureDatabases = [ cfg.database.name ]; 251 ensureUsers = [ 252 { 253 name = cfg.database.user; 254 ensureDBOwnership = true; 255 } 256 ]; 257 }; 258 259 services.redis = lib.optionalAttrs cfg.redis.createLocally { 260 servers."${cfg.redis.name}" = { 261 enable = true; 262 port = cfg.redis.port; 263 }; 264 }; 265 266 systemd.services.zammad-web = { 267 inherit environment; 268 serviceConfig = serviceConfig // { 269 # loading all the gems takes time 270 TimeoutStartSec = 1200; 271 }; 272 after = 273 [ 274 "network.target" 275 "systemd-tmpfiles-setup.service" 276 ] 277 ++ lib.optionals (cfg.database.createLocally) [ 278 "postgresql.service" 279 ] 280 ++ lib.optionals cfg.redis.createLocally [ 281 "redis-${cfg.redis.name}.service" 282 ]; 283 requires = lib.optionals (cfg.database.createLocally) [ 284 "postgresql.service" 285 ]; 286 description = "Zammad web"; 287 wantedBy = [ "multi-user.target" ]; 288 preStart = '' 289 # config file 290 cat ${databaseConfig} > ${cfg.dataDir}/config/database.yml 291 ${lib.optionalString (cfg.database.passwordFile != null) '' 292 { 293 echo -n " password: " 294 cat ${cfg.database.passwordFile} 295 } >> ${cfg.dataDir}/config/database.yml 296 ''} 297 ${lib.optionalString (cfg.secretKeyBaseFile != null) '' 298 { 299 echo "production: " 300 echo -n " secret_key_base: " 301 cat ${cfg.secretKeyBaseFile} 302 } > ${cfg.dataDir}/config/secrets.yml 303 ''} 304 305 # needed for cleanup 306 shopt -s extglob 307 308 # cleanup state directory from module before refactoring in 309 # https://github.com/NixOS/nixpkgs/pull/277456 310 if [[ -e ${cfg.dataDir}/node_modules ]]; then 311 rm -rf ${cfg.dataDir}/!("tmp"|"config"|"log"|"state_dir_migrated"|"db_seeded") 312 rm -rf ${cfg.dataDir}/config/!("database.yml"|"secrets.yml") 313 # state directory cleanup required --> zammad was already installed --> do not seed db 314 echo true > ${cfg.dataDir}/db_seeded 315 fi 316 317 SEEDED=$(cat ${cfg.dataDir}/db_seeded) 318 if [[ $SEEDED != "true" ]]; then 319 echo "Initialize database" 320 ./bin/rake --no-system db:migrate 321 ./bin/rake --no-system db:seed 322 echo true > ${cfg.dataDir}/db_seeded 323 else 324 echo "Migrate database" 325 ./bin/rake --no-system db:migrate 326 fi 327 echo "Done" 328 ''; 329 script = "./script/rails server -b ${cfg.host} -p ${toString cfg.port}"; 330 }; 331 332 systemd.tmpfiles.rules = [ 333 "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -" 334 "d ${cfg.dataDir}/config 0750 ${cfg.user} ${cfg.group} - -" 335 "d ${cfg.dataDir}/tmp 0750 ${cfg.user} ${cfg.group} - -" 336 "d ${cfg.dataDir}/log 0750 ${cfg.user} ${cfg.group} - -" 337 "f ${cfg.dataDir}/config/secrets.yml 0640 ${cfg.user} ${cfg.group} - -" 338 "f ${cfg.dataDir}/config/database.yml 0640 ${cfg.user} ${cfg.group} - -" 339 "f ${cfg.dataDir}/db_seeded 0640 ${cfg.user} ${cfg.group} - -" 340 ]; 341 342 systemd.services.zammad-websocket = { 343 inherit serviceConfig environment; 344 after = [ "zammad-web.service" ]; 345 requires = [ "zammad-web.service" ]; 346 description = "Zammad websocket"; 347 wantedBy = [ "multi-user.target" ]; 348 script = "./script/websocket-server.rb -b ${cfg.host} -p ${toString cfg.websocketPort} start"; 349 }; 350 351 systemd.services.zammad-worker = { 352 inherit serviceConfig environment; 353 after = [ "zammad-web.service" ]; 354 requires = [ "zammad-web.service" ]; 355 description = "Zammad background worker"; 356 wantedBy = [ "multi-user.target" ]; 357 script = "./script/background-worker.rb start"; 358 }; 359 }; 360 361 meta.maintainers = with lib.maintainers; [ 362 taeer 363 netali 364 ]; 365}