Luke Granger-Brown
57725ef3ec
git-subtree-dir: third_party/nixpkgs git-subtree-split: 76612b17c0ce71689921ca12d9ffdc9c23ce40b2
515 lines
18 KiB
Nix
515 lines
18 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.snipe-it;
|
|
snipe-it = pkgs.snipe-it.override {
|
|
dataDir = cfg.dataDir;
|
|
};
|
|
db = cfg.database;
|
|
mail = cfg.mail;
|
|
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
|
|
tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
|
|
|
|
inherit (snipe-it.passthru) phpPackage;
|
|
|
|
# shell script for local administration
|
|
artisan = (pkgs.writeScriptBin "snipe-it" ''
|
|
#! ${pkgs.runtimeShell}
|
|
cd "${snipe-it}/share/php/snipe-it"
|
|
sudo=exec
|
|
if [[ "$USER" != ${user} ]]; then
|
|
sudo='exec /run/wrappers/bin/sudo -u ${user}'
|
|
fi
|
|
$sudo ${phpPackage}/bin/php artisan $*
|
|
'').overrideAttrs (old: {
|
|
meta = old.meta // {
|
|
mainProgram = "snipe-it";
|
|
};
|
|
});
|
|
in {
|
|
options.services.snipe-it = {
|
|
|
|
enable = mkEnableOption "snipe-it, a free open source IT asset/license management system";
|
|
|
|
user = mkOption {
|
|
default = "snipeit";
|
|
description = "User snipe-it runs as.";
|
|
type = types.str;
|
|
};
|
|
|
|
group = mkOption {
|
|
default = "snipeit";
|
|
description = "Group snipe-it runs as.";
|
|
type = types.str;
|
|
};
|
|
|
|
appKeyFile = mkOption {
|
|
description = ''
|
|
A file containing the Laravel APP_KEY - a 32 character long,
|
|
base64 encoded key used for encryption where needed. Can be
|
|
generated with `head -c 32 /dev/urandom | base64`.
|
|
'';
|
|
example = "/run/keys/snipe-it/appkey";
|
|
type = types.path;
|
|
};
|
|
|
|
hostName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = config.networking.fqdnOrHostName;
|
|
defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
|
|
example = "snipe-it.example.com";
|
|
description = ''
|
|
The hostname to serve Snipe-IT on.
|
|
'';
|
|
};
|
|
|
|
appURL = mkOption {
|
|
description = ''
|
|
The root URL that you want to host Snipe-IT on. All URLs in Snipe-IT will be generated using this value.
|
|
If you change this in the future you may need to run a command to update stored URLs in the database.
|
|
Command example: `snipe-it snipe-it:update-url https://old.example.com https://new.example.com`
|
|
'';
|
|
default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostName}";
|
|
defaultText = ''
|
|
http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostName}
|
|
'';
|
|
example = "https://example.com";
|
|
type = types.str;
|
|
};
|
|
|
|
dataDir = mkOption {
|
|
description = "snipe-it data directory";
|
|
default = "/var/lib/snipe-it";
|
|
type = types.path;
|
|
};
|
|
|
|
database = {
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = "Database host address.";
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 3306;
|
|
description = "Database host port.";
|
|
};
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "snipeit";
|
|
description = "Database name.";
|
|
};
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = user;
|
|
defaultText = literalExpression "user";
|
|
description = "Database username.";
|
|
};
|
|
passwordFile = mkOption {
|
|
type = with types; nullOr path;
|
|
default = null;
|
|
example = "/run/keys/snipe-it/dbpassword";
|
|
description = ''
|
|
A file containing the password corresponding to
|
|
{option}`database.user`.
|
|
'';
|
|
};
|
|
createLocally = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Create the database and database user locally.";
|
|
};
|
|
};
|
|
|
|
mail = {
|
|
driver = mkOption {
|
|
type = types.enum [ "smtp" "sendmail" ];
|
|
default = "smtp";
|
|
description = "Mail driver to use.";
|
|
};
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = "Mail host address.";
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 1025;
|
|
description = "Mail host port.";
|
|
};
|
|
encryption = mkOption {
|
|
type = with types; nullOr (enum [ "tls" "ssl" ]);
|
|
default = null;
|
|
description = "SMTP encryption mechanism to use.";
|
|
};
|
|
user = mkOption {
|
|
type = with types; nullOr str;
|
|
default = null;
|
|
example = "snipeit";
|
|
description = "Mail username.";
|
|
};
|
|
passwordFile = mkOption {
|
|
type = with types; nullOr path;
|
|
default = null;
|
|
example = "/run/keys/snipe-it/mailpassword";
|
|
description = ''
|
|
A file containing the password corresponding to
|
|
{option}`mail.user`.
|
|
'';
|
|
};
|
|
backupNotificationAddress = mkOption {
|
|
type = types.str;
|
|
default = "backup@example.com";
|
|
description = "Email Address to send Backup Notifications to.";
|
|
};
|
|
from = {
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "Snipe-IT Asset Management";
|
|
description = "Mail \"from\" name.";
|
|
};
|
|
address = mkOption {
|
|
type = types.str;
|
|
default = "mail@example.com";
|
|
description = "Mail \"from\" address.";
|
|
};
|
|
};
|
|
replyTo = {
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "Snipe-IT Asset Management";
|
|
description = "Mail \"reply-to\" name.";
|
|
};
|
|
address = mkOption {
|
|
type = types.str;
|
|
default = "mail@example.com";
|
|
description = "Mail \"reply-to\" address.";
|
|
};
|
|
};
|
|
};
|
|
|
|
maxUploadSize = mkOption {
|
|
type = types.str;
|
|
default = "18M";
|
|
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 ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
|
|
);
|
|
default = {};
|
|
example = literalExpression ''
|
|
{
|
|
serverAliases = [
|
|
"snipe-it.''${config.networking.domain}"
|
|
];
|
|
# To enable encryption and let let's encrypt take care of certificate
|
|
forceSSL = true;
|
|
enableACME = true;
|
|
}
|
|
'';
|
|
description = ''
|
|
With this option, you can customize the nginx virtualHost settings.
|
|
'';
|
|
};
|
|
|
|
config = mkOption {
|
|
type = with types;
|
|
attrsOf
|
|
(nullOr
|
|
(either
|
|
(oneOf [
|
|
bool
|
|
int
|
|
port
|
|
path
|
|
str
|
|
])
|
|
(submodule {
|
|
options = {
|
|
_secret = mkOption {
|
|
type = nullOr (oneOf [ str path ]);
|
|
description = ''
|
|
The path to a file containing the value the
|
|
option should be set to in the final
|
|
configuration file.
|
|
'';
|
|
};
|
|
};
|
|
})));
|
|
default = {};
|
|
example = literalExpression ''
|
|
{
|
|
ALLOWED_IFRAME_HOSTS = "https://example.com";
|
|
WKHTMLTOPDF = "''${pkgs.wkhtmltopdf}/bin/wkhtmltopdf";
|
|
AUTH_METHOD = "oidc";
|
|
OIDC_NAME = "MyLogin";
|
|
OIDC_DISPLAY_NAME_CLAIMS = "name";
|
|
OIDC_CLIENT_ID = "snipe-it";
|
|
OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
|
|
OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
|
|
OIDC_ISSUER_DISCOVER = true;
|
|
}
|
|
'';
|
|
description = ''
|
|
Snipe-IT configuration options to set in the
|
|
{file}`.env` file.
|
|
Refer to <https://snipe-it.readme.io/docs/configuration>
|
|
for details on supported values.
|
|
|
|
Settings containing secret data should be set to an attribute
|
|
set containing the attribute `_secret` - a
|
|
string pointing to a file containing the value the option
|
|
should be set to. See the example to get a better picture of
|
|
this: in the resulting {file}`.env` file, the
|
|
`OIDC_CLIENT_SECRET` key will be set to the
|
|
contents of the {file}`/run/keys/oidc_secret`
|
|
file.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
assertions = [
|
|
{ assertion = db.createLocally -> db.user == user;
|
|
message = "services.snipe-it.database.user must be set to ${user} if services.snipe-it.database.createLocally is set true.";
|
|
}
|
|
{ assertion = db.createLocally -> db.passwordFile == null;
|
|
message = "services.snipe-it.database.passwordFile cannot be specified if services.snipe-it.database.createLocally is set to true.";
|
|
}
|
|
];
|
|
|
|
environment.systemPackages = [ artisan ];
|
|
|
|
services.snipe-it.config = {
|
|
APP_ENV = "production";
|
|
APP_KEY._secret = cfg.appKeyFile;
|
|
APP_URL = cfg.appURL;
|
|
DB_HOST = db.host;
|
|
DB_PORT = db.port;
|
|
DB_DATABASE = db.name;
|
|
DB_USERNAME = db.user;
|
|
DB_PASSWORD._secret = db.passwordFile;
|
|
MAIL_DRIVER = mail.driver;
|
|
MAIL_FROM_NAME = mail.from.name;
|
|
MAIL_FROM_ADDR = mail.from.address;
|
|
MAIL_REPLYTO_NAME = mail.from.name;
|
|
MAIL_REPLYTO_ADDR = mail.from.address;
|
|
MAIL_BACKUP_NOTIFICATION_ADDRESS = mail.backupNotificationAddress;
|
|
MAIL_HOST = mail.host;
|
|
MAIL_PORT = mail.port;
|
|
MAIL_USERNAME = mail.user;
|
|
MAIL_ENCRYPTION = mail.encryption;
|
|
MAIL_PASSWORD._secret = mail.passwordFile;
|
|
APP_SERVICES_CACHE = "/run/snipe-it/cache/services.php";
|
|
APP_PACKAGES_CACHE = "/run/snipe-it/cache/packages.php";
|
|
APP_CONFIG_CACHE = "/run/snipe-it/cache/config.php";
|
|
APP_ROUTES_CACHE = "/run/snipe-it/cache/routes-v7.php";
|
|
APP_EVENTS_CACHE = "/run/snipe-it/cache/events.php";
|
|
SECURE_COOKIES = tlsEnabled;
|
|
};
|
|
|
|
services.mysql = mkIf db.createLocally {
|
|
enable = true;
|
|
package = mkDefault pkgs.mariadb;
|
|
ensureDatabases = [ db.name ];
|
|
ensureUsers = [
|
|
{ name = db.user;
|
|
ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
|
|
}
|
|
];
|
|
};
|
|
|
|
services.phpfpm.pools.snipe-it = {
|
|
inherit user group phpPackage;
|
|
phpOptions = ''
|
|
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}/share/php/snipe-it/public";
|
|
extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
|
|
locations = {
|
|
"/" = {
|
|
index = "index.php";
|
|
extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
|
|
};
|
|
"~ \.php$" = {
|
|
extraConfig = ''
|
|
try_files $uri $uri/ /index.php?$query_string;
|
|
include ${config.services.nginx.package}/conf/fastcgi_params;
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
fastcgi_param REDIRECT_STATUS 200;
|
|
fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket};
|
|
${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
|
|
'';
|
|
};
|
|
"~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
|
|
extraConfig = "expires 365d;";
|
|
};
|
|
};
|
|
}];
|
|
};
|
|
|
|
systemd.services.snipe-it-setup = {
|
|
description = "Preparation tasks for snipe-it";
|
|
before = [ "phpfpm-snipe-it.service" ];
|
|
after = optional db.createLocally "mysql.service";
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
User = user;
|
|
WorkingDirectory = snipe-it;
|
|
RuntimeDirectory = "snipe-it/cache";
|
|
RuntimeDirectoryMode = "0700";
|
|
};
|
|
path = [ pkgs.replace-secret artisan ];
|
|
script =
|
|
let
|
|
isSecret = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
|
|
snipeITEnvVars = lib.generators.toKeyValue {
|
|
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
|
mkValueString = v: with builtins;
|
|
if isInt v then toString v
|
|
else if isString v then "\"${v}\""
|
|
else if true == v then "true"
|
|
else if false == v then "false"
|
|
else if isSecret v then
|
|
if (isString v._secret) then
|
|
hashString "sha256" v._secret
|
|
else
|
|
hashString "sha256" (builtins.readFile v._secret)
|
|
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
|
|
};
|
|
};
|
|
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
|
|
mkSecretReplacement = file: ''
|
|
replace-secret ${escapeShellArgs [
|
|
(
|
|
if (isString file) then
|
|
builtins.hashString "sha256" file
|
|
else
|
|
builtins.hashString "sha256" (builtins.readFile file)
|
|
)
|
|
file
|
|
"${cfg.dataDir}/.env"
|
|
]}
|
|
'';
|
|
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
|
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
|
|
snipeITEnv = pkgs.writeText "snipeIT.env" (snipeITEnvVars filteredConfig);
|
|
in ''
|
|
# error handling
|
|
set -euo pipefail
|
|
|
|
# set permissions
|
|
umask 077
|
|
|
|
# create .env file
|
|
install -T -m 0600 -o ${user} ${snipeITEnv} "${cfg.dataDir}/.env"
|
|
|
|
# replace secrets
|
|
${secretReplacements}
|
|
|
|
# prepend `base64:` if it does not exist in APP_KEY
|
|
if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
|
|
sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
|
|
fi
|
|
|
|
# purge cache
|
|
rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
|
|
|
|
# migrate db
|
|
${lib.getExe artisan} migrate --force
|
|
|
|
# A placeholder file for invalid barcodes
|
|
invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
|
|
if [ ! -e "$invalid_barcode_location" ]; then
|
|
cp ${snipe-it}/share/snipe-it/invalid_barcode.gif "$invalid_barcode_location"
|
|
fi
|
|
'';
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.dataDir} 0710 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/bootstrap 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/bootstrap/cache 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/accessories 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/assets 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/avatars 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/barcodes 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/categories 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/companies 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/components 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/consumables 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/departments 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/locations 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/manufacturers 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/models 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/public/uploads/suppliers 0750 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/storage 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/storage/fonts 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/uploads 0700 ${user} ${group} - -"
|
|
"d ${cfg.dataDir}/storage/private_uploads 0700 ${user} ${group} - -"
|
|
];
|
|
|
|
users = {
|
|
users = mkIf (user == "snipeit") {
|
|
snipeit = {
|
|
inherit group;
|
|
isSystemUser = true;
|
|
};
|
|
"${config.services.nginx.user}".extraGroups = [ group ];
|
|
};
|
|
groups = mkIf (group == "snipeit") {
|
|
snipeit = {};
|
|
};
|
|
};
|
|
|
|
};
|
|
|
|
meta.maintainers = with maintainers; [ yayayayaka ];
|
|
}
|