diff --git a/flake.nix b/flake.nix index 13a1c41..5e9ceee 100644 --- a/flake.nix +++ b/flake.nix @@ -19,6 +19,8 @@ nixosModules = { ryzenSMU = import ./modules/ryzenSMU; + + servicesSnipeIT = import ./modules/services/snipe-it.nix; }; packages = forAllSystems (system: import ./. { diff --git a/modules/services/snipe-it.nix b/modules/services/snipe-it.nix new file mode 100644 index 0000000..941f741 --- /dev/null +++ b/modules/services/snipe-it.nix @@ -0,0 +1,517 @@ +{ config, lib, pkgs, modulesPath, ... }: + +let + inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types + boolToString callPackageWith optional optionals optionalString recursiveUpdate; + + cfg = config.services.snipe-it; + + phpPackage = cfg.phpPackage.withExtensions ({ enabled, all }: + enabled ++ (with all; [ + json + openssl + pdo + mbstring + tokenizer + curl + ldap + zip + fileinfo + bcmath + gd + ] ++ optionals usePgsql [ + pdo_pgsql + ] ++ optionals useMysql [ + pdo_mysql + ]) + ); + + nginxPackage = config.services.nginx.package; + + user = cfg.user; + group = cfg.group; + + db = cfg.database; + usePgsql = db.type == "pgsql"; + useMysql = db.type == "mysql"; + useSqlite = db.type == "sqlite"; + + mail = cfg.mail; + + useSSL = with cfg.nginx; (addSSL || forceSSL || onlySSL || enableACME); + + snipe-it = cfg.package.override { inherit (cfg) cacheDir dataDir; }; + + artisan = pkgs.writeShellScriptBin "snipe-it" '' + cd ${snipe-it} + sudo=exec + if [[ "$USER" != ${user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${user}' + fi + $sudo ${phpPackage}/bin/php artisan $* + ''; + +in { + + options.services.snipe-it = { + enable = mkEnableOption "Snipe-IT free open source IT asset management"; + + package = mkOption { + type = types.package; + default = callPackageWith pkgs ../../pkgs/snipe-it { }; + description = "Snipe-IT derivation to use."; + }; + + phpPackage = mkOption { + type = types.package; + default = pkgs.php74; + description = "PHP package to use."; + }; + + user = mkOption { + type = types.str; + default = "snipe-it"; + description = "User Snipe-IT runs as."; + }; + + group = mkOption { + type = types.str; + default = "snipe-it"; + description = "Group Snipe-IT runs as."; + }; + + hostName = mkOption { + type = types.str; + example = "assets.example.com"; + description = "FQDN for the Snipe-IT instance."; + }; + + maxResults = mkOption { + type = types.int; + default = 500; + description = '' + The result limit. This value determines the maximum number of results to return, + even if a higher limit is passed in an API request. This is done to prevent + timeouts when custom scripts are requesting large numbers of assets at a time. + ''; + }; + + # Basic app settings + appKeyFile = mkOption { + type = types.path; + example = "/run/keys/snipe-it-appkey"; + description = '' + A file containing the app key. Used for encryption where needed. + Can be generated with head -c32 /dev/urandom | base64. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/snipe-it"; + description = "Snipe-IT's data directory."; + }; + + cacheDir = mkOption { + type = types.path; + default = "/var/cache/snipe-it"; + description = "Snipe-IT's cache directory"; + }; + + database = { + type = mkOption { + type = types.enum [ "pgsql" "mysql" "sqlite" ]; + default = "pgsql"; + description = "Database engine to use."; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + port = mkOption { + type = types.port; + default = if useMysql then 3306 else 5432; + description = '' + Database host port. This currently only has + an effect when using MySQL. + ''; + }; + name = mkOption { + type = types.str; + default = "snipe-it"; + description = "Name of the PostgreSQL or MySQL database."; + }; + username = mkOption { + type = types.str; + default = user; + description = "Username to use to connect to database."; + }; + passwordFile = mkOption { + type = types.nullOr types.path; + example = "/run/keys/snipe-it-dbpass"; + description = '' + File containing the password corresponding to + . + ''; + }; + }; + + mail = { + host = mkOption { + type = types.str; + default = "localhost"; + description = "Mail server host address."; + }; + port = mkOption { + type = types.port; + default = 25; + description = "Mail server host port."; + }; + encryption = mkOption { + type = types.nullOr (types.enum [ "ssl" "tls" ]); + default = null; + example = "tls"; + description = '' + Type of transport encryption to use. + ''; + }; + username = mkOption { + type = types.nullOr types.str; + default = null; + description = "User to use to connect to mail server."; + }; + passwordFile = mkOption { + type = types.path; + example = "/run/keys/snipe-it-mailpass"; + description = '' + File containing the password corresponding to + . + ''; + }; + fromAddress = mkOption { + type = types.str; + description = ''Global "From" address.''; + }; + fromName = mkOption { + type = types.str; + default = "Snipe-IT"; + description = ''Global "From" name.''; + }; + replytoAddress = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Global "Reply-To" address. If null (the default), + "Reply-To" won't be set. + ''; + }; + replytoName = mkOption { + type = types.str; + default = "Snipe-IT"; + description = '' + Global "Reply-To" name. + also has to be set for this to have any effect. + ''; + }; + autoEmbed = { + enable = mkEnableOption "Embed images into mails instead of hyperlinking them."; + + method = mkOption { + type = types.enum [ "attachment" "base64" ]; + default = "attachment"; + description = "Embedding method to use."; + }; + }; + }; + + memcached = { + enable = mkEnableOption "memcached as caching backend"; + host = mkOption { + type = types.str; + default = "localhost"; + description = "Memcached host address."; + }; + + port = mkOption { + type = types.port; + default = 11211; + description = "Memcached host port."; + }; + }; + + session = { + lifeTime = mkOption { + type = types.int; + default = 12000; + description = "Session lifetime in minutes."; + }; + expireOnClose = mkOption { + type = types.bool; + default = false; + description = "Expire sessions when closing browser window."; + }; + encrypt = mkOption { + type = types.bool; + default = false; + description = "Encrypt stored session data."; + }; + }; + + maxUploadSize = mkOption { + type = types.str; + default = "16M"; + example = "1G"; + description = "The maximum size for uploads (e.g. images)."; + }; + + 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 Snipe-IT PHP pool. See the documentation on + php-fpm.conf for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import + (modulesPath + "/services/web-servers/nginx/vhost-options.nix") + { inherit config lib; }) + {} + ); + default = {}; + example = { + serverAliases = [ + "snipe-it.\${config.networking.domain}" + ]; + # To enable encryption and let letsencrypt take care of certificate + forceSSL = true; + enableACME = true; + }; + description = '' + With this option, you can customize the nginz virtualHost settings. + ''; + }; + + extraConfig = mkOption { + type = types.nullOr types.lines; + default = null; + example = '' + LOGIN_MAX_ATTEMPTS=3 + LOGIN_LOCKOUT_DURATION=300 + ''; + }; + + }; + + config = mkIf cfg.enable { + assertions = [ + { assertion = usePgsql -> db.port == 5432; + message = "PostgreSQL is currently only supported with default port 5432."; + } + ]; + + warnings = + optional (!useMysql) '' + Please note: Using another database than MySQL isn't officially supported. + ''; + + environment.systemPackages = [ artisan ]; + + services.postgresql = mkIf (usePgsql && db.host == "localhost") { + enable = mkDefault true; + ensureDatabases = [ db.name ]; + ensureUsers = [{ + name = db.username; + ensurePermissions = { "DATABASE \"${db.name}\"" = "ALL PRIVILEGES"; }; + }]; + }; + + services.mysql = mkIf (useMysql && db.host == "localhost") { + enable = mkDefault true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [ db.name ]; + ensureUsers = [{ + name = db.username; + ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; }; + }]; + }; + + services.phpfpm.pools.snipe-it = { + inherit user group; + phpOptions = '' + log_errors = on + post_max_size = ${cfg.maxUploadSize} + upload_max_filesize = ${cfg.maxUploadSize} + ''; + settings = { + "listen.mode" = "0660"; + "listen.owner" = user; + "listen.group" = group; + } // cfg.poolConfig; + }; + + services.nginx = { + enable = mkDefault true; + virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx { + root = mkForce "${snipe-it}/public"; + extraConfig = '' + index index.php index.html index.htm; + ${optionalString useSSL "fastcgi_param HTTPS on;"} + ''; + locations = { + "/" = { + extraConfig = ''try_files $uri $uri/ /index.php$is_args$args;''; + }; + "~ \.php$" = { + extraConfig = '' + try_files $uri $uri/ =404; + include ${nginxPackage}/conf/fastcgi_params; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket}; + ${optionalString useSSL "fastcgi_param HTTPS on;"} + ''; + }; + }; + }]; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.cacheDir} 0700 ${user} ${group} - -" + "d ${cfg.cacheDir}/bootstrap 0700 ${user} ${group} - -" + "d ${cfg.dataDir} 0710 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/accessories 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/assets 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/avatars 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/barcodes 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/categories 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/companies 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/components 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/consumables 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/departments 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/locations 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/manufacturers 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/models 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/uploads/suppliers 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app/backups 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app/backups/env-backups 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/debugbar 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/assetmodels 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/assets 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/audits 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/imports 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/licenses 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/signatures 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/private_uploads/users 0700 ${user} ${group} - -" + ] ++ optionals useSqlite [ + "f ${cfg.dataDir}/database.sqlite 0600 ${user} ${group} -" + ]; + + + systemd.services.snipe-it-setup = { + description = "Preparation tasks for Snipe-IT"; + before = [ "phpfpm-snipe-it.service" ]; + after = optional useMysql "mysql.service" + ++ optional usePgsql "postgresql.service"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + User = user; + WorkingDirectory = "${snipe-it}"; + }; + script = '' + # create .env file + cat > ${cfg.dataDir}/.env << EOF + APP_KEY="base64:$(head -n1 ${cfg.appKeyFile})" + APP_URL="http${optionalString useSSL "s"}://${cfg.hostName}" + APP_LOG=syslog + MAX_RESULTS=${toString cfg.maxResults} + + '' + optionalString useSSL '' + ENABLE_HSTS=true + SECURE_COOKIES=true + + '' + '' + SESSION_DRIVER=file + SESSION_LIFETIME=${toString cfg.session.lifeTime} + EXPIRE_ON_CLOSE=${boolToString cfg.session.expireOnClose} + ENCRYPT=${boolToString cfg.session.encrypt} + + DB_CONNECTION=${db.type} + '' + optionalString (db.type != "sqlite") '' + DB_HOST=${db.host} + DB_PORT=${toString db.port} + DB_PASSWORD="$(head -n1 ${db.passwordFile})" + DB_USERNAME=${db.username} + DB_DATABASE=${db.name} + '' + '' + + MAIL_DRIVER=smtp + MAIL_HOST=${mail.host} + MAIL_PORT=${toString mail.port} + ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption}"} + MAIL_FROM_ADDR=${mail.fromAddress} + MAIL_FROM_NAME=${mail.fromName} + MAIL_AUTO_EMBED=${boolToString mail.autoEmbed.enable} + MAIL_AUTO_EMBED_METHOD=${mail.autoEmbed.method} + '' + optionalString (mail.username != null) '' + MAIL_USERNAME=${mail.username} + MAIL_PASSWORD="$(head -n1 ${mail.passwordFile})" + '' + optionalString (mail.replytoAddress != null) '' + MAIL_REPLYTO_ADDR=${mail.replytoAddress} + MAIL_REPLYTO_NAME=${mail.replytoName} + '' + optionalString cfg.memcached.enable '' + + CACHE_DRIVER=memcached + MEMCACHED_HOST=${cfg.memcached.host} + MEMCACHED_PORT=${toString cfg.memcached.port} + '' + '' + + ${optionalString (cfg.extraConfig != null) cfg.extraConfig} + EOF + chmod 600 ${cfg.dataDir}/.env + + # re-evaluate configuration + ${phpPackage}/bin/php artisan config:clear + ${phpPackage}/bin/php artisan config:cache + + # migrate db + ${phpPackage}/bin/php artisan migrate --force + + # create caches + ${phpPackage}/bin/php artisan event:cache + ${phpPackage}/bin/php artisan view:cache + ''; + }; + + users = { + users."${user}" = { + isSystemUser = true; + home = cfg.dataDir; + group = group; + }; + groups."${group}" = {}; + users."${config.services.nginx.user}".extraGroups = [ group ]; + }; + }; +}