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 ];
+ };
+ };
+}