packages/modules/services/snipe-it.nix

501 lines
16 KiB
Nix

{ 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; [
openssl
pdo
mbstring
tokenizer
curl
ldap
zip
fileinfo
bcmath
gd
] ++ optionals useMysql [
pdo_mysql
])
);
nginxPackage = config.services.nginx.package;
user = cfg.user;
group = cfg.group;
db = cfg.database;
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;
php = phpPackage; phpPackages = phpPackage.packages;
};
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.php80;
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 <code>head -c32 /dev/urandom | base64</code>.
'';
};
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 [ "mysql" "sqlite" ];
default = "mysql";
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 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
<option>database.username</option>.
'';
};
};
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
<option>mail.username</option>.
'';
};
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. <option>mail.replytoAddress</option>
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
<literal>php-fpm.conf</literal> 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 {
warnings =
optional (!useMysql) ''
Please note: Using another database than MySQL isn't officially supported.
'';
environment.systemPackages = [ artisan ];
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 phpPackage;
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";
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 ];
};
};
}