push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.airsonic;
opt = options.services.airsonic;
in
{
options = {
services.airsonic = {
enable = lib.mkEnableOption "Airsonic, the Free and Open Source media streaming server (fork of Subsonic and Libresonic)";
user = lib.mkOption {
type = lib.types.str;
default = "airsonic";
description = "User account under which airsonic runs.";
};
home = lib.mkOption {
type = lib.types.path;
default = "/var/lib/airsonic";
description = ''
The directory where Airsonic will create files.
Make sure it is writable.
'';
};
virtualHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
The host name or IP address on which to bind Airsonic.
The default value is appropriate for first launch, when the
default credentials are easy to guess. It is also appropriate
if you intend to use the virtualhost option in the service
module. In other cases, you may want to change this to a
specific IP or 0.0.0.0 to listen on all interfaces.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 4040;
description = ''
The port on which Airsonic will listen for
incoming HTTP traffic. Set to 0 to disable.
'';
};
contextPath = lib.mkOption {
type = lib.types.path;
default = "/";
description = ''
The context path, i.e., the last part of the Airsonic
URL. Typically '/' or '/airsonic'. Default '/'
'';
};
maxMemory = lib.mkOption {
type = lib.types.int;
default = 100;
description = ''
The memory limit (max Java heap size) in megabytes.
Default: 100
'';
};
transcoders = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
defaultText = lib.literalExpression ''[ "''${pkgs.ffmpeg.bin}/bin/ffmpeg" ]'';
description = ''
List of paths to transcoder executables that should be accessible
from Airsonic. Symlinks will be created to each executable inside
''${config.${opt.home}}/transcoders.
'';
};
jre = lib.mkPackageOption pkgs "jre8" {
extraDescription = ''
::: {.note}
Airsonic only supports Java 8, airsonic-advanced requires at least
Java 11.
:::
'';
};
war = lib.mkOption {
type = lib.types.path;
default = "${pkgs.airsonic}/webapps/airsonic.war";
defaultText = lib.literalExpression ''"''${pkgs.airsonic}/webapps/airsonic.war"'';
description = "Airsonic war file to use.";
};
jvmOptions = lib.mkOption {
description = ''
Extra command line options for the JVM running AirSonic.
Useful for sending jukebox output to non-default alsa
devices.
'';
default = [
];
type = lib.types.listOf lib.types.str;
example = [
"-Djavax.sound.sampled.Clip='#CODEC [plughw:1,0]'"
"-Djavax.sound.sampled.Port='#Port CODEC [hw:1]'"
"-Djavax.sound.sampled.SourceDataLine='#CODEC [plughw:1,0]'"
"-Djavax.sound.sampled.TargetDataLine='#CODEC [plughw:1,0]'"
];
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.airsonic = {
description = "Airsonic Media Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
# Install transcoders.
rm -rf ${cfg.home}/transcode
mkdir -p ${cfg.home}/transcode
for exe in ${toString cfg.transcoders}; do
ln -sf "$exe" ${cfg.home}/transcode
done
'';
serviceConfig = {
ExecStart = ''
${cfg.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
-Dairsonic.home=${cfg.home} \
-Dserver.address=${cfg.listenAddress} \
-Dserver.port=${toString cfg.port} \
-Dserver.context-path=${cfg.contextPath} \
-Djava.awt.headless=true \
${lib.optionalString (cfg.virtualHost != null) "-Dserver.use-forward-headers=true"} \
${toString cfg.jvmOptions} \
-verbose:gc \
-jar ${cfg.war}
'';
Restart = "always";
User = "airsonic";
UMask = "0022";
};
};
services.nginx = lib.mkIf (cfg.virtualHost != null) {
enable = true;
recommendedProxySettings = true;
virtualHosts.${cfg.virtualHost} = {
locations.${cfg.contextPath}.proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
};
};
users.users.airsonic = {
description = "Airsonic service user";
group = "airsonic";
name = cfg.user;
home = cfg.home;
createHome = true;
isSystemUser = true;
};
users.groups.airsonic = { };
};
}

View File

@@ -0,0 +1,95 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.amazon-ssm-agent;
# The SSM agent doesn't pay attention to our /etc/os-release yet, and the lsb-release tool
# in nixpkgs doesn't seem to work properly on NixOS, so let's just fake the two fields SSM
# looks for. See https://github.com/aws/amazon-ssm-agent/issues/38 for upstream fix.
fake-lsb-release = pkgs.writeScriptBin "lsb_release" ''
#!${pkgs.runtimeShell}
case "$1" in
-i) echo "nixos";;
-r) echo "${config.system.nixos.version}";;
esac
'';
sudoRule = {
users = [ "ssm-user" ];
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
};
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "ssm-agent" "enable" ]
[ "services" "amazon-ssm-agent" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "ssm-agent" "package" ]
[ "services" "amazon-ssm-agent" "package" ]
)
];
options.services.amazon-ssm-agent = {
enable = lib.mkEnableOption "Amazon SSM agent";
package = lib.mkPackageOption pkgs "amazon-ssm-agent" { };
};
config = lib.mkIf cfg.enable {
# See https://github.com/aws/amazon-ssm-agent/blob/mainline/packaging/linux/amazon-ssm-agent.service
systemd.services.amazon-ssm-agent = {
inherit (cfg.package.meta) description;
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [
fake-lsb-release
pkgs.coreutils
"/run/wrappers"
"/run/current-system/sw"
];
serviceConfig = {
ExecStart = "${cfg.package}/bin/amazon-ssm-agent";
KillMode = "process";
# We want this restating pretty frequently. It could be our only means
# of accessing the instance.
Restart = "always";
RestartPreventExitStatus = 194;
RestartSec = "90";
};
};
# Add user that Session Manager needs, and give it sudo.
# This is consistent with Amazon Linux 2 images.
security.sudo.extraRules = [ sudoRule ];
security.sudo-rs.extraRules = [ sudoRule ];
# On Amazon Linux 2 images, the ssm-user user is pretty much a
# normal user with its own group. We do the same.
users.groups.ssm-user = { };
users.users.ssm-user = {
isNormalUser = true;
group = "ssm-user";
};
environment.etc."amazon/ssm/seelog.xml".source =
"${cfg.package}/etc/amazon/ssm/seelog.xml.template";
environment.etc."amazon/ssm/amazon-ssm-agent.json".source =
"${cfg.package}/etc/amazon/ssm/amazon-ssm-agent.json.template";
};
}

View File

@@ -0,0 +1,175 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ananicy;
configFile = pkgs.writeText "ananicy.conf" (lib.generators.toKeyValue { } cfg.settings);
extraRules = pkgs.writeText "extraRules" (
lib.concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.extraRules
);
extraTypes = pkgs.writeText "extraTypes" (
lib.concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.extraTypes
);
extraCgroups = pkgs.writeText "extraCgroups" (
lib.concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.extraCgroups
);
servicename =
if ((lib.getName cfg.package) == (lib.getName pkgs.ananicy-cpp)) then "ananicy-cpp" else "ananicy";
in
{
options.services.ananicy = {
enable = lib.mkEnableOption "Ananicy, an auto nice daemon";
package = lib.mkPackageOption pkgs "ananicy" { example = "ananicy-cpp"; };
rulesProvider = lib.mkPackageOption pkgs "ananicy" { example = "ananicy-cpp"; } // {
description = ''
Which package to copy default rules,types,cgroups from.
'';
};
settings = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
int
bool
str
]);
default = { };
example = {
apply_nice = false;
};
description = ''
See <https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf>
'';
};
extraRules = lib.mkOption {
type = with lib.types; listOf attrs;
default = [ ];
description = ''
Rules to write in 'nixRules.rules'. See:
<https://github.com/Nefelim4ag/Ananicy#configuration>
<https://gitlab.com/ananicy-cpp/ananicy-cpp/#global-configuration>
'';
example = [
{
name = "eog";
type = "Image-Viewer";
}
{
name = "fdupes";
type = "BG_CPUIO";
}
];
};
extraTypes = lib.mkOption {
type = with lib.types; listOf attrs;
default = [ ];
description = ''
Types to write in 'nixTypes.types'. See:
<https://gitlab.com/ananicy-cpp/ananicy-cpp/#types>
'';
example = [
{
type = "my_type";
nice = 19;
other_parameter = "value";
}
{
type = "compiler";
nice = 19;
sched = "batch";
ioclass = "idle";
}
];
};
extraCgroups = lib.mkOption {
type = with lib.types; listOf attrs;
default = [ ];
description = ''
Cgroups to write in 'nixCgroups.cgroups'. See:
<https://gitlab.com/ananicy-cpp/ananicy-cpp/#cgroups>
'';
example = [
{
cgroup = "cpu80";
CPUQuota = 80;
}
];
};
};
config = lib.mkIf cfg.enable {
environment = {
systemPackages = [ cfg.package ];
etc."ananicy.d".source =
pkgs.runCommand "ananicyfiles"
{
preferLocalBuild = true;
}
''
mkdir -p $out
# ananicy-cpp does not include rules or settings on purpose
if [[ -d "${cfg.rulesProvider}/etc/ananicy.d/00-default" ]]; then
cp -r ${cfg.rulesProvider}/etc/ananicy.d/* $out
else
cp -r ${cfg.rulesProvider}/* $out
fi
# configured through .setings
rm -f $out/ananicy.conf
cp ${configFile} $out/ananicy.conf
${lib.optionalString (cfg.extraRules != [ ]) "cp ${extraRules} $out/nixRules.rules"}
${lib.optionalString (cfg.extraTypes != [ ]) "cp ${extraTypes} $out/nixTypes.types"}
${lib.optionalString (cfg.extraCgroups != [ ]) "cp ${extraCgroups} $out/nixCgroups.cgroups"}
'';
};
# ananicy and ananicy-cpp have different default settings
services.ananicy.settings =
let
mkOD = lib.mkOptionDefault;
in
{
cgroup_load = mkOD true;
type_load = mkOD true;
rule_load = mkOD true;
apply_nice = mkOD true;
apply_ioclass = mkOD true;
apply_ionice = mkOD true;
apply_sched = mkOD true;
apply_oom_score_adj = mkOD true;
apply_cgroup = mkOD true;
}
// (
if servicename == "ananicy-cpp" then
{
# https://gitlab.com/ananicy-cpp/ananicy-cpp/-/blob/master/src/config.cpp#L12
loglevel = mkOD "warn"; # default is info but its spammy
cgroup_realtime_workaround = true;
log_applied_rule = mkOD false;
}
else
{
# https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf
check_disks_schedulers = mkOD true;
check_freq = mkOD 5;
}
);
systemd = {
packages = [ cfg.package ];
services."${servicename}" = {
wantedBy = [ "default.target" ];
};
};
};
meta.maintainers = with lib.maintainers; [ artturin ];
}

View File

@@ -0,0 +1,128 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.angrr;
in
{
meta.maintainers = pkgs.angrr.meta.maintainers;
options = {
services.angrr = {
enable = lib.mkEnableOption "angrr";
package = lib.mkPackageOption pkgs "angrr" { };
period = lib.mkOption {
type = lib.types.str;
default = "7d";
example = "2weeks";
description = ''
The retention period of auto GC roots.
'';
};
removeRoot = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to pass the `--remove-root` option to angrr.
'';
};
ownedOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Control the `--remove-root=<true|false>` option of angrr.
'';
apply = b: if b then "true" else "false";
};
logLevel = lib.mkOption {
type =
with lib.types;
enum [
"off"
"error"
"warn"
"info"
"debug"
"trace"
];
default = "info";
description = ''
Set the log level of angrr.
'';
};
extraArgs = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = ''
Extra command-line arguments pass to angrr.
'';
};
enableNixGcIntegration = lib.mkOption {
type = lib.types.bool;
description = ''
Whether to enable nix-gc.service integration
'';
};
timer = {
enable = lib.mkEnableOption "angrr timer";
dates = lib.mkOption {
type = lib.types.str;
default = "03:00";
description = ''
How often or when the retention policy is performed.
'';
};
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
assertions = [
{
assertion = cfg.enableNixGcIntegration -> config.nix.gc.automatic;
message = "angrr nix-gc.service integration requires `nix.gc.automatic = true`";
}
];
services.angrr.enableNixGcIntegration = lib.mkDefault config.nix.gc.automatic;
}
{
systemd.services.angrr = {
description = "Auto Nix GC Roots Retention";
script = ''
${lib.getExe cfg.package} run \
--log-level "${cfg.logLevel}" \
--period "${cfg.period}" \
${lib.optionalString cfg.removeRoot "--remove-root"} \
--owned-only="${cfg.ownedOnly}" \
--no-prompt ${lib.escapeShellArgs cfg.extraArgs}
'';
serviceConfig = {
Type = "oneshot";
};
};
}
(lib.mkIf cfg.timer.enable {
systemd.timers.angrr = {
timerConfig = {
OnCalendar = cfg.timer.dates;
};
wantedBy = [ "timers.target" ];
};
})
(lib.mkIf cfg.enableNixGcIntegration {
systemd.services.angrr = {
wantedBy = [ "nix-gc.service" ];
before = [ "nix-gc.service" ];
};
})
]
);
}

View File

@@ -0,0 +1,65 @@
# Anki Sync Server {#module-services-anki-sync-server}
[Anki Sync Server](https://docs.ankiweb.net/sync-server.html) is the built-in
sync server, present in recent versions of Anki. Advanced users who cannot or
do not wish to use AnkiWeb can use this sync server instead of AnkiWeb.
This module is compatible only with Anki versions >=2.1.66, due to [recent
enhancements to the Nix anki
package](https://github.com/NixOS/nixpkgs/commit/05727304f8815825565c944d012f20a9a096838a).
## Basic Usage {#module-services-anki-sync-server-basic-usage}
By default, the module creates a
[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/)
unit which runs the sync server with an isolated user using the systemd
`DynamicUser` option.
This can be done by enabling the `anki-sync-server` service:
```nix
{ ... }:
{
services.anki-sync-server.enable = true;
}
```
It is necessary to set at least one username-password pair under
{option}`services.anki-sync-server.users`. For example
```nix
{
services.anki-sync-server.users = [
{
username = "user";
passwordFile = /etc/anki-sync-server/user;
}
];
}
```
Here, `passwordFile` is the path to a file containing just the password in
plaintext. Make sure to set permissions to make this file unreadable to any
user besides root.
By default, synced data are stored in */var/lib/anki-sync-server/*ankiuser**.
You can change the directory by using `services.anki-sync-server.baseDirectory`
```nix
{ services.anki-sync-server.baseDirectory = "/home/anki/data"; }
```
By default, the server listen address {option}`services.anki-sync-server.host`
is set to localhost, listening on port
{option}`services.anki-sync-server.port`, and does not open the firewall. This
is suitable for purely local testing, or to be used behind a reverse proxy. If
you want to expose the sync server directly to other computers (not recommended
in most circumstances, because the sync server doesn't use HTTPS), then set the
following options:
```nix
{
services.anki-sync-server.address = "0.0.0.0";
services.anki-sync-server.openFirewall = true;
}
```

View File

@@ -0,0 +1,144 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.anki-sync-server;
name = "anki-sync-server";
specEscape = replaceStrings [ "%" ] [ "%%" ];
usersWithIndexes = lists.imap1 (i: user: {
i = i;
user = user;
}) cfg.users;
usersWithIndexesFile = filter (x: x.user.passwordFile != null) usersWithIndexes;
usersWithIndexesNoFile = filter (
x: x.user.passwordFile == null && x.user.password != null
) usersWithIndexes;
anki-sync-server-run = pkgs.writeShellScript "anki-sync-server-run" ''
# When services.anki-sync-server.users.passwordFile is set,
# each password file is passed as a systemd credential, which is mounted in
# a file system exposed to the service. Here we read the passwords from
# the credential files to pass them as environment variables to the Anki
# sync server.
${concatMapStringsSep "\n" (x: ''
read -r pass < "''${CREDENTIALS_DIRECTORY}/"${escapeShellArg x.user.username}
export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:"$pass"
'') usersWithIndexesFile}
# For users where services.anki-sync-server.users.password isn't set,
# export passwords in environment variables in plaintext.
${concatMapStringsSep "\n" (
x:
''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:${escapeShellArg x.user.password}''
) usersWithIndexesNoFile}
exec ${lib.getExe cfg.package}
'';
in
{
options.services.anki-sync-server = {
enable = mkEnableOption "anki-sync-server";
package = mkPackageOption pkgs "anki-sync-server" { };
address = mkOption {
type = types.str;
default = "::1";
description = ''
IP address anki-sync-server listens to.
Note host names are not resolved.
'';
};
port = mkOption {
type = types.port;
default = 27701;
description = "Port number anki-sync-server listens to.";
};
baseDirectory = mkOption {
type = types.str;
default = "%S/%N";
description = "Base directory where user(s) synchronized data will be stored.";
};
openFirewall = mkOption {
default = false;
type = types.bool;
description = "Whether to open the firewall for the specified port.";
};
users = mkOption {
type =
with types;
listOf (submodule {
options = {
username = mkOption {
type = str;
description = "User name accepted by anki-sync-server.";
};
password = mkOption {
type = nullOr str;
default = null;
description = ''
Password accepted by anki-sync-server for the associated username.
**WARNING**: This option is **not secure**. This password will
be stored in *plaintext* and will be visible to *all users*.
See {option}`services.anki-sync-server.users.passwordFile` for
a more secure option.
'';
};
passwordFile = mkOption {
type = nullOr path;
default = null;
description = ''
File containing the password accepted by anki-sync-server for
the associated username. Make sure to make readable only by
root.
'';
};
};
});
description = "List of user-password pairs to provide to the sync server.";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (builtins.length usersWithIndexesFile) + (builtins.length usersWithIndexesNoFile) > 0;
message = "At least one username-password pair must be set.";
}
];
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
systemd.services.anki-sync-server = {
description = "anki-sync-server: Anki sync server built into Anki";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ cfg.package ];
environment = {
SYNC_BASE = cfg.baseDirectory;
SYNC_HOST = specEscape cfg.address;
SYNC_PORT = toString cfg.port;
};
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = name;
ExecStart = anki-sync-server-run;
Restart = "always";
LoadCredential = map (
x: "${specEscape x.user.username}:${specEscape (toString x.user.passwordFile)}"
) usersWithIndexesFile;
};
};
};
meta = {
maintainers = with maintainers; [ telotortium ];
doc = ./anki-sync-server.md;
};
}

View File

@@ -0,0 +1,253 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.apache-kafka;
# The `javaProperties` generator takes care of various escaping rules and
# generation of the properties file, but we'll handle stringly conversion
# ourselves in mkPropertySettings and stringlySettings, since we know more
# about the specifically allowed format eg. for lists of this type, and we
# don't want to coerce-downsample values to str too early by having the
# coercedTypes from javaProperties directly in our NixOS option types.
#
# Make sure every `freeformType` and any specific option type in `settings` is
# supported here.
mkPropertyString =
let
render = {
bool = lib.boolToString;
int = toString;
list = lib.concatMapStringsSep "," mkPropertyString;
string = lib.id;
};
in
v: render.${builtins.typeOf v} v;
stringlySettings = lib.mapAttrs (_: mkPropertyString) (
lib.filterAttrs (_: v: v != null) cfg.settings
);
generator = (pkgs.formats.javaProperties { }).generate;
in
{
options.services.apache-kafka = {
enable = lib.mkEnableOption "Apache Kafka event streaming broker";
settings = lib.mkOption {
description = ''
[Kafka broker configuration](https://kafka.apache.org/documentation.html#brokerconfigs)
{file}`server.properties`.
Note that .properties files contain mappings from string to string.
Keys with dots are NOT represented by nested attrs in these settings,
but instead as quoted strings (ie. `settings."broker.id"`, NOT
`settings.broker.id`).
'';
type = lib.types.submodule {
freeformType =
with lib.types;
let
primitive = oneOf [
bool
int
str
];
in
lazyAttrsOf (nullOr (either primitive (listOf primitive)));
options = {
"broker.id" = lib.mkOption {
description = "Broker ID. -1 or null to auto-allocate in zookeeper mode.";
default = null;
type = with lib.types; nullOr int;
};
"log.dirs" = lib.mkOption {
description = "Log file directories.";
# Deliberaly leave out old default and use the rewrite opportunity
# to have users choose a safer value -- /tmp might be volatile and is a
# slightly scary default choice.
# default = [ "/tmp/apache-kafka" ];
type = with lib.types; listOf path;
};
"listeners" = lib.mkOption {
description = ''
Kafka Listener List.
See [listeners](https://kafka.apache.org/documentation/#brokerconfigs_listeners).
'';
type = lib.types.listOf lib.types.str;
default = [ "PLAINTEXT://localhost:9092" ];
};
};
};
};
clusterId = lib.mkOption {
description = ''
KRaft mode ClusterId used for formatting log directories. Can be generated with `kafka-storage.sh random-uuid`
'';
type = with lib.types; nullOr str;
default = null;
};
configFiles.serverProperties = lib.mkOption {
description = ''
Kafka server.properties configuration file path.
Defaults to the rendered `settings`.
'';
type = lib.types.path;
};
configFiles.log4jProperties = lib.mkOption {
description = "Kafka log4j property configuration file path";
type = lib.types.path;
default = pkgs.writeText "log4j.properties" cfg.log4jProperties;
defaultText = ''pkgs.writeText "log4j.properties" cfg.log4jProperties'';
};
formatLogDirs = lib.mkOption {
description = ''
Whether to format log dirs in KRaft mode if all log dirs are
unformatted, ie. they contain no meta.properties.
'';
type = lib.types.bool;
default = false;
};
formatLogDirsIgnoreFormatted = lib.mkOption {
description = ''
Whether to ignore already formatted log dirs when formatting log dirs,
instead of failing. Useful when replacing or adding disks.
'';
type = lib.types.bool;
default = false;
};
log4jProperties = lib.mkOption {
description = "Kafka log4j property configuration.";
default = ''
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n
'';
type = lib.types.lines;
};
jvmOptions = lib.mkOption {
description = "Extra command line options for the JVM running Kafka.";
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"-Djava.net.preferIPv4Stack=true"
"-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.local.only=true"
];
};
package = lib.mkPackageOption pkgs "apacheKafka" { };
jre = lib.mkOption {
description = "The JRE with which to run Kafka";
default = cfg.package.passthru.jre;
defaultText = lib.literalExpression "pkgs.apacheKafka.passthru.jre";
type = lib.types.package;
};
};
imports = [
(lib.mkRenamedOptionModule
[ "services" "apache-kafka" "brokerId" ]
[ "services" "apache-kafka" "settings" ''broker.id'' ]
)
(lib.mkRenamedOptionModule
[ "services" "apache-kafka" "logDirs" ]
[ "services" "apache-kafka" "settings" ''log.dirs'' ]
)
(lib.mkRenamedOptionModule
[ "services" "apache-kafka" "zookeeper" ]
[ "services" "apache-kafka" "settings" ''zookeeper.connect'' ]
)
(lib.mkRemovedOptionModule [
"services"
"apache-kafka"
"port"
] "Please see services.apache-kafka.settings.listeners and its documentation instead")
(lib.mkRemovedOptionModule [
"services"
"apache-kafka"
"hostname"
] "Please see services.apache-kafka.settings.listeners and its documentation instead")
(lib.mkRemovedOptionModule [
"services"
"apache-kafka"
"extraProperties"
] "Please see services.apache-kafka.settings and its documentation instead")
(lib.mkRemovedOptionModule [
"services"
"apache-kafka"
"serverProperties"
] "Please see services.apache-kafka.settings and its documentation instead")
];
config = lib.mkIf cfg.enable {
services.apache-kafka.configFiles.serverProperties = generator "server.properties" stringlySettings;
users.users.apache-kafka = {
isSystemUser = true;
group = "apache-kafka";
description = "Apache Kafka daemon user";
};
users.groups.apache-kafka = { };
systemd.tmpfiles.rules = map (
logDir: "d '${logDir}' 0700 apache-kafka - - -"
) cfg.settings."log.dirs";
systemd.services.apache-kafka = {
description = "Apache Kafka Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = lib.mkIf cfg.formatLogDirs (
if cfg.formatLogDirsIgnoreFormatted then
''
${cfg.package}/bin/kafka-storage.sh format -t "${cfg.clusterId}" -c ${cfg.configFiles.serverProperties} --ignore-formatted
''
else
''
if ${
lib.concatMapStringsSep " && " (l: ''[ ! -f "${l}/meta.properties" ]'') cfg.settings."log.dirs"
}; then
${cfg.package}/bin/kafka-storage.sh format -t "${cfg.clusterId}" -c ${cfg.configFiles.serverProperties}
fi
''
);
serviceConfig = {
ExecStart = ''
${cfg.jre}/bin/java \
-cp "${cfg.package}/libs/*" \
-Dlog4j.configuration=file:${cfg.configFiles.log4jProperties} \
${toString cfg.jvmOptions} \
kafka.Kafka \
${cfg.configFiles.serverProperties}
'';
User = "apache-kafka";
SuccessExitStatus = "0 143";
};
};
};
meta.doc = ./kafka.md;
meta.maintainers = with lib.maintainers; [
srhb
];
}

View File

@@ -0,0 +1,162 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib) mkOption types mkIf;
cfg = config.services.atuin;
in
{
options = {
services.atuin = {
enable = lib.mkEnableOption "Atuin server for shell history sync";
package = lib.mkPackageOption pkgs "atuin" { };
openRegistration = mkOption {
type = types.bool;
default = false;
description = "Allow new user registrations with the atuin server.";
};
path = mkOption {
type = types.str;
default = "";
description = "A path to prepend to all the routes of the server.";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The host address the atuin server should listen on.";
};
maxHistoryLength = mkOption {
type = types.int;
default = 8192;
description = "The max length of each history item the atuin server should store.";
};
port = mkOption {
type = types.port;
default = 8888;
description = "The port the atuin server should listen on.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the atuin server.";
};
database = {
createLocally = mkOption {
type = types.bool;
default = true;
description = "Create the database and database user locally.";
};
uri = mkOption {
type = types.nullOr types.str;
default = "postgresql:///atuin?host=/run/postgresql";
example = "postgresql://atuin@localhost:5432/atuin";
description = ''
URI to the database.
Can be set to null in which case ATUIN_DB_URI should be set through an EnvironmentFile
'';
};
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.database.createLocally -> config.services.postgresql.enable;
message = "Postgresql must be enabled to create a local database";
}
];
services.postgresql = mkIf cfg.database.createLocally {
enable = true;
ensureUsers = [
{
name = "atuin";
ensureDBOwnership = true;
}
];
ensureDatabases = [ "atuin" ];
};
systemd.services.atuin = {
description = "atuin server";
requires = lib.optionals cfg.database.createLocally [ "postgresql.target" ];
after = [
"network-online.target"
]
++ lib.optionals cfg.database.createLocally [ "postgresql.target" ];
wants = [
"network-online.target"
]
++ lib.optionals cfg.database.createLocally [ "postgresql.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} server start";
RuntimeDirectory = "atuin";
RuntimeDirectoryMode = "0700";
DynamicUser = true;
# Hardening
CapabilityBoundingSet = "";
LockPersonality = true;
NoNewPrivileges = true;
MemoryDenyWriteExecute = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
# Required for connecting to database sockets,
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
environment = {
ATUIN_HOST = cfg.host;
ATUIN_PORT = toString cfg.port;
ATUIN_MAX_HISTORY_LENGTH = toString cfg.maxHistoryLength;
ATUIN_OPEN_REGISTRATION = lib.boolToString cfg.openRegistration;
ATUIN_PATH = cfg.path;
ATUIN_CONFIG_DIR = "/run/atuin"; # required to start, but not used as configuration is via environment variables
}
// lib.optionalAttrs (cfg.database.uri != null) { ATUIN_DB_URI = cfg.database.uri; };
};
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
};
}

View File

@@ -0,0 +1,86 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.autobrr;
configFormat = pkgs.formats.toml { };
configTemplate = configFormat.generate "autobrr.toml" cfg.settings;
templaterCmd = ''${lib.getExe pkgs.dasel} put -f '${configTemplate}' -v "$(${config.systemd.package}/bin/systemd-creds cat sessionSecret)" -o %S/autobrr/config.toml "sessionSecret"'';
in
{
options = {
services.autobrr = {
enable = lib.mkEnableOption "Autobrr";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the Autobrr web interface.";
};
secretFile = lib.mkOption {
type = lib.types.path;
description = "File containing the session secret for the Autobrr web interface.";
};
settings = lib.mkOption {
type = lib.types.submodule { freeformType = configFormat.type; };
default = {
host = "127.0.0.1";
port = 7474;
checkForUpdates = true;
};
example = {
logLevel = "DEBUG";
};
description = ''
Autobrr configuration options.
Refer to <https://autobrr.com/configuration/autobrr>
for a full list.
'';
};
package = lib.mkPackageOption pkgs "autobrr" { };
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.settings ? sessionSecret);
message = ''
Session secrets should not be passed via settings, as
these are stored in the world-readable nix store.
Use the secretFile option instead.'';
}
];
systemd.services.autobrr = {
description = "Autobrr";
after = [
"syslog.target"
"network-online.target"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
DynamicUser = true;
LoadCredential = "sessionSecret:${cfg.secretFile}";
StateDirectory = "autobrr";
ExecStartPre = "${lib.getExe pkgs.bash} -c '${templaterCmd}'";
ExecStart = "${lib.getExe cfg.package} --config %S/autobrr";
Restart = "on-failure";
};
};
networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.settings.port ]; };
};
}

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.autofs;
autoMaster = pkgs.writeText "auto.master" cfg.autoMaster;
in
{
###### interface
options = {
services.autofs = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Mount filesystems on demand. Unmount them automatically.
You may also be interested in afuse.
'';
};
autoMaster = lib.mkOption {
type = lib.types.str;
example = lib.literalExpression ''
let
mapConf = pkgs.writeText "auto" '''
kernel -ro,soft,intr ftp.kernel.org:/pub/linux
boot -fstype=ext2 :/dev/hda1
windoze -fstype=smbfs ://windoze/c
removable -fstype=ext2 :/dev/hdd
cd -fstype=iso9660,ro :/dev/hdc
floppy -fstype=auto :/dev/fd0
server -rw,hard,intr / -ro myserver.me.org:/ \
/usr myserver.me.org:/usr \
/home myserver.me.org:/home
''';
in '''
/auto file:''${mapConf}
'''
'';
description = ''
Contents of `/etc/auto.master` file. See {manpage}`auto.master(5)` and {manpage}`autofs(5)`.
'';
};
timeout = lib.mkOption {
type = lib.types.int;
default = 600;
description = "Set the global minimum timeout, in seconds, until directories are unmounted";
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Pass -d and -7 to automount and write log to the system journal.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
boot.kernelModules = [ "autofs" ];
systemd.services.autofs = {
description = "Automounts filesystems on demand";
after = [
"network.target"
"ypbind.service"
"sssd.service"
"network-online.target"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
# There should be only one autofs service managed by systemd, so this should be safe.
rm -f /tmp/autofs-running
'';
serviceConfig = {
Type = "forking";
PIDFile = "/run/autofs.pid";
ExecStart = "${pkgs.autofs5}/bin/automount ${lib.optionalString cfg.debug "-d"} -p /run/autofs.pid -t ${builtins.toString cfg.timeout} ${autoMaster}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View File

@@ -0,0 +1,409 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.autorandr;
hookType = lib.types.lines;
matrixOf =
n: m: elemType:
lib.mkOptionType rec {
name = "matrixOf";
description = "${toString n}×${toString m} matrix of ${elemType.description}s";
check =
xss:
let
listOfSize = l: xs: lib.isList xs && lib.length xs == l;
in
listOfSize n xss && lib.all (xs: listOfSize m xs && lib.all elemType.check xs) xss;
merge = lib.mergeOneOption;
getSubOptions =
prefix:
elemType.getSubOptions (
prefix
++ [
"*"
"*"
]
);
getSubModules = elemType.getSubModules;
substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
functor = (lib.defaultFunctor name) // {
wrapped = elemType;
};
};
profileModule = lib.types.submodule {
options = {
fingerprint = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = ''
Output name to EDID mapping.
Use `autorandr --fingerprint` to get current setup values.
'';
default = { };
};
config = lib.mkOption {
type = lib.types.attrsOf configModule;
description = "Per output profile configuration.";
default = { };
};
hooks = lib.mkOption {
type = hooksModule;
description = "Profile hook scripts.";
default = { };
};
};
};
configModule = lib.types.submodule {
options = {
enable = lib.mkOption {
type = lib.types.bool;
description = "Whether to enable the output.";
default = true;
};
crtc = lib.mkOption {
type = lib.types.nullOr lib.types.ints.unsigned;
description = "Output video display controller.";
default = null;
example = 0;
};
primary = lib.mkOption {
type = lib.types.bool;
description = "Whether output should be marked as primary";
default = false;
};
position = lib.mkOption {
type = lib.types.str;
description = "Output position";
default = "";
example = "5760x0";
};
mode = lib.mkOption {
type = lib.types.str;
description = "Output resolution.";
default = "";
example = "3840x2160";
};
rate = lib.mkOption {
type = lib.types.str;
description = "Output framerate.";
default = "";
example = "60.00";
};
gamma = lib.mkOption {
type = lib.types.str;
description = "Output gamma configuration.";
default = "";
example = "1.0:0.909:0.833";
};
rotate = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"normal"
"left"
"right"
"inverted"
]
);
description = "Output rotate configuration.";
default = null;
example = "left";
};
transform = lib.mkOption {
type = lib.types.nullOr (matrixOf 3 3 lib.types.float);
default = null;
example = lib.literalExpression ''
[
[ 0.6 0.0 0.0 ]
[ 0.0 0.6 0.0 ]
[ 0.0 0.0 1.0 ]
]
'';
description = ''
Refer to
{manpage}`xrandr(1)`
for the documentation of the transform matrix.
'';
};
dpi = lib.mkOption {
type = lib.types.nullOr lib.types.ints.positive;
description = "Output DPI configuration.";
default = null;
example = 96;
};
scale = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
method = lib.mkOption {
type = lib.types.enum [
"factor"
"pixel"
];
description = "Output scaling method.";
default = "factor";
example = "pixel";
};
x = lib.mkOption {
type = lib.types.either lib.types.float lib.types.ints.positive;
description = "Horizontal scaling factor/pixels.";
};
y = lib.mkOption {
type = lib.types.either lib.types.float lib.types.ints.positive;
description = "Vertical scaling factor/pixels.";
};
};
}
);
description = ''
Output scale configuration.
Either configure by pixels or a scaling factor. When using pixel method the
{manpage}`xrandr(1)`
option
`--scale-from`
will be used; when using factor method the option
`--scale`
will be used.
This option is a shortcut version of the transform option and they are mutually
exclusive.
'';
default = null;
example = lib.literalExpression ''
{
x = 1.25;
y = 1.25;
}
'';
};
};
};
hooksModule = lib.types.submodule {
options = {
postswitch = lib.mkOption {
type = lib.types.attrsOf hookType;
description = "Postswitch hook executed after mode switch.";
default = { };
};
preswitch = lib.mkOption {
type = lib.types.attrsOf hookType;
description = "Preswitch hook executed before mode switch.";
default = { };
};
predetect = lib.mkOption {
type = lib.types.attrsOf hookType;
description = ''
Predetect hook executed before autorandr attempts to run xrandr.
'';
default = { };
};
};
};
hookToFile =
folder: name: hook:
lib.nameValuePair "xdg/autorandr/${folder}/${name}" {
source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
};
profileToFiles =
name: profile:
with profile;
lib.mkMerge [
{
"xdg/autorandr/${name}/setup".text = lib.concatStringsSep "\n" (
lib.mapAttrsToList fingerprintToString fingerprint
);
"xdg/autorandr/${name}/config".text = lib.concatStringsSep "\n" (
lib.mapAttrsToList configToString profile.config
);
}
(lib.mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
(lib.mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
(lib.mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
];
fingerprintToString = name: edid: "${name} ${edid}";
configToString =
name: config:
if config.enable then
lib.concatStringsSep "\n" (
[ "output ${name}" ]
++ lib.optional (config.position != "") "pos ${config.position}"
++ lib.optional (config.crtc != null) "crtc ${toString config.crtc}"
++ lib.optional config.primary "primary"
++ lib.optional (config.dpi != null) "dpi ${toString config.dpi}"
++ lib.optional (config.gamma != "") "gamma ${config.gamma}"
++ lib.optional (config.mode != "") "mode ${config.mode}"
++ lib.optional (config.rate != "") "rate ${config.rate}"
++ lib.optional (config.rotate != null) "rotate ${config.rotate}"
++ lib.optional (config.transform != null) (
"transform " + lib.concatMapStringsSep "," toString (lib.flatten config.transform)
)
++ lib.optional (config.scale != null) (
(if config.scale.method == "factor" then "scale" else "scale-from")
+ " ${toString config.scale.x}x${toString config.scale.y}"
)
)
else
''
output ${name}
off
'';
in
{
options = {
services.autorandr = {
enable = lib.mkEnableOption "handling of hotplug and sleep events by autorandr";
defaultTarget = lib.mkOption {
default = "default";
type = lib.types.str;
description = ''
Fallback if no monitor layout can be detected. See the docs
(https://github.com/phillipberndt/autorandr/blob/v1.0/README.md#how-to-use)
for further reference.
'';
};
ignoreLid = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Treat outputs as connected even if their lids are closed";
};
matchEdid = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Match displays based on edid instead of name";
};
hooks = lib.mkOption {
type = hooksModule;
description = "Global hook scripts";
default = { };
example = lib.literalExpression ''
{
postswitch = {
"notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
"change-background" = readFile ./change-background.sh;
"change-dpi" = '''
case "$AUTORANDR_CURRENT_PROFILE" in
default)
DPI=120
;;
home)
DPI=192
;;
work)
DPI=144
;;
*)
echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
exit 1
esac
echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
''';
};
}
'';
};
profiles = lib.mkOption {
type = lib.types.attrsOf profileModule;
description = "Autorandr profiles specification.";
default = { };
example = lib.literalExpression ''
{
"work" = {
fingerprint = {
eDP1 = "<EDID>";
DP1 = "<EDID>";
};
config = {
eDP1.enable = false;
DP1 = {
enable = true;
crtc = 0;
primary = true;
position = "0x0";
mode = "3840x2160";
gamma = "1.0:0.909:0.833";
rate = "60.00";
rotate = "left";
};
};
hooks.postswitch = readFile ./work-postswitch.sh;
};
}
'';
};
};
};
config = lib.mkIf cfg.enable {
services.udev.packages = [ pkgs.autorandr ];
environment = {
systemPackages = [ pkgs.autorandr ];
etc = lib.mkMerge [
(lib.mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
(lib.mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
(lib.mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
(lib.mkMerge (lib.mapAttrsToList profileToFiles cfg.profiles))
];
};
systemd.services.autorandr = {
wantedBy = [ "sleep.target" ];
description = "Autorandr execution hook";
after = [ "sleep.target" ];
startLimitIntervalSec = 5;
startLimitBurst = 1;
serviceConfig = {
ExecStart = ''
${pkgs.autorandr}/bin/autorandr \
--batch \
--change \
--default ${cfg.defaultTarget} \
${lib.optionalString cfg.ignoreLid "--ignore-lid"} \
${lib.optionalString cfg.matchEdid "--match-edid"}
'';
Type = "oneshot";
RemainAfterExit = false;
KillMode = "process";
};
};
};
meta.maintainers = with lib.maintainers; [ alexnortung ];
}

View File

@@ -0,0 +1,253 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
mapAttrs'
nameValuePair
filterAttrs
types
mkEnableOption
mkPackageOption
mkOption
literalExpression
mkIf
flatten
maintainers
attrValues
;
cfg = config.services.autosuspend;
settingsFormat = pkgs.formats.ini { };
checks = mapAttrs' (n: v: nameValuePair "check.${n}" (filterAttrs (_: v: v != null) v)) cfg.checks;
wakeups = mapAttrs' (
n: v: nameValuePair "wakeup.${n}" (filterAttrs (_: v: v != null) v)
) cfg.wakeups;
# Whether the given check is enabled
hasCheck =
class:
(filterAttrs (n: v: v.enabled && (if v.class == null then n else v.class) == class) cfg.checks)
!= { };
# Dependencies needed by specific checks
dependenciesForChecks = {
"Smb" = pkgs.samba;
"XIdleTime" = [
pkgs.xprintidle
pkgs.sudo
];
};
autosuspend-conf = settingsFormat.generate "autosuspend.conf" (
{ general = cfg.settings; } // checks // wakeups
);
autosuspend = cfg.package;
checkType = types.submodule {
freeformType = settingsFormat.type.nestedTypes.elemType;
options.enabled = mkEnableOption "this activity check" // {
default = true;
};
options.class = mkOption {
default = null;
type =
with types;
nullOr (enum [
"ActiveCalendarEvent"
"ActiveConnection"
"ExternalCommand"
"JsonPath"
"Kodi"
"KodiIdleTime"
"LastLogActivity"
"Load"
"LogindSessionsIdle"
"Mpd"
"NetworkBandwidth"
"Ping"
"Processes"
"Smb"
"Users"
"XIdleTime"
"XPath"
]);
description = ''
Name of the class implementing the check. If this option is not specified, the check's
name must represent a valid internal check class.
'';
};
};
wakeupType = types.submodule {
freeformType = settingsFormat.type.nestedTypes.elemType;
options.enabled = mkEnableOption "this wake-up check" // {
default = true;
};
options.class = mkOption {
default = null;
type =
with types;
nullOr (enum [
"Calendar"
"Command"
"File"
"Periodic"
"SystemdTimer"
"XPath"
"XPathDelta"
]);
description = ''
Name of the class implementing the check. If this option is not specified, the check's
name must represent a valid internal check class.
'';
};
};
in
{
options = {
services.autosuspend = {
enable = mkEnableOption "the autosuspend daemon";
package = mkPackageOption pkgs "autosuspend" { };
settings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type.nestedTypes.elemType;
options = {
# Provide reasonable defaults for these two (required) options
suspend_cmd = mkOption {
default = "systemctl suspend";
type = with types; str;
description = ''
The command to execute in case the host shall be suspended. This line can contain
additional command line arguments to the command to execute.
'';
};
wakeup_cmd = mkOption {
default = ''sh -c 'echo 0 > /sys/class/rtc/rtc0/wakealarm && echo {timestamp:.0f} > /sys/class/rtc/rtc0/wakealarm' '';
type = with types; str;
description = ''
The command to execute for scheduling a wake up of the system. The given string is
processed using Pythons `str.format()` and a format argument called `timestamp`
encodes the UTC timestamp of the planned wake up time (float). Additionally `iso`
can be used to acquire the timestamp in ISO 8601 format.
'';
};
};
};
default = { };
example = literalExpression ''
{
enable = true;
interval = 30;
idle_time = 120;
}
'';
description = ''
Configuration for autosuspend, see
<https://autosuspend.readthedocs.io/en/latest/configuration_file.html#general-configuration>
for supported values.
'';
};
checks = mkOption {
default = { };
type = with types; attrsOf checkType;
description = ''
Checks for activity. For more information, see:
- <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#activity-check-configuration>
- <https://autosuspend.readthedocs.io/en/latest/available_checks.html>
'';
example = literalExpression ''
{
# Basic activity check configuration.
# The check class name is derived from the section header (Ping in this case).
# Remember to enable desired checks. They are disabled by default.
Ping = {
hosts = "192.168.0.7";
};
# This check is disabled.
Smb.enabled = false;
# Example for a custom check name.
# This will use the Users check with the custom name RemoteUsers.
# Custom names are necessary in case a check class is used multiple times.
# Custom names can also be used for clarification.
RemoteUsers = {
class = "Users";
name = ".*";
terminal = ".*";
host = "[0-9].*";
};
# Here the Users activity check is used again with different settings and a different name
LocalUsers = {
class = "Users";
name = ".*";
terminal = ".*";
host = "localhost";
};
}
'';
};
wakeups = mkOption {
default = { };
type = with types; attrsOf wakeupType;
description = ''
Checks for wake up. For more information, see:
- <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#wake-up-check-configuration>
- <https://autosuspend.readthedocs.io/en/latest/available_wakeups.html>
'';
example = literalExpression ''
{
# Wake up checks reuse the same configuration mechanism as activity checks.
Calendar = {
url = "http://example.org/test.ics";
};
}
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.autosuspend = {
description = "A daemon to suspend your server in case of inactivity";
documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = flatten (attrValues (filterAttrs (n: _: hasCheck n) dependenciesForChecks));
serviceConfig = {
ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} daemon'';
};
};
systemd.services.autosuspend-detect-suspend = {
description = "Notifies autosuspend about suspension";
documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
wantedBy = [ "sleep.target" ];
after = [ "sleep.target" ];
serviceConfig = {
ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} presuspend'';
};
};
};
meta = {
maintainers = with maintainers; [ xlambein ];
};
}

View File

@@ -0,0 +1,93 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.bazarr;
in
{
options = {
services.bazarr = {
enable = lib.mkEnableOption "bazarr, a subtitle manager for Sonarr and Radarr";
package = lib.mkPackageOption pkgs "bazarr" { };
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/bazarr";
description = "The directory where Bazarr stores its data files.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the bazarr web interface.";
};
listenPort = lib.mkOption {
type = lib.types.port;
default = 6767;
description = "Port on which the bazarr web interface should listen";
};
user = lib.mkOption {
type = lib.types.str;
default = "bazarr";
description = "User account under which bazarr runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "bazarr";
description = "Group under which bazarr runs.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.settings."10-bazarr".${cfg.dataDir}.d = {
inherit (cfg) user group;
mode = "0700";
};
systemd.services.bazarr = {
description = "Bazarr";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
SyslogIdentifier = "bazarr";
ExecStart = pkgs.writeShellScript "start-bazarr" ''
${cfg.package}/bin/bazarr \
--config '${cfg.dataDir}' \
--port ${toString cfg.listenPort} \
--no-update True
'';
Restart = "on-failure";
KillSignal = "SIGINT";
SuccessExitStatus = "0 156";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listenPort ];
};
users.users = lib.mkIf (cfg.user == "bazarr") {
bazarr = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
};
};
users.groups = lib.mkIf (cfg.group == "bazarr") {
bazarr = { };
};
};
}

View File

@@ -0,0 +1,176 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.bcg;
configFile = (pkgs.formats.yaml { }).generate "bcg.conf.yaml" (
lib.filterAttrsRecursive (n: v: v != null) {
inherit (cfg) device name mqtt;
retain_node_messages = cfg.retainNodeMessages;
qos_node_messages = cfg.qosNodeMessages;
base_topic_prefix = cfg.baseTopicPrefix;
automatic_remove_kit_from_names = cfg.automaticRemoveKitFromNames;
automatic_rename_kit_nodes = cfg.automaticRenameKitNodes;
automatic_rename_generic_nodes = cfg.automaticRenameGenericNodes;
automatic_rename_nodes = cfg.automaticRenameNodes;
}
);
in
{
options = {
services.bcg = {
enable = lib.mkEnableOption "BigClown gateway";
package = lib.mkPackageOption pkgs [ "python3Packages" "bcg" ] { };
environmentFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
example = [ "/run/keys/bcg.env" ];
description = ''
File to load as environment file. Environment variables from this file
will be interpolated into the config file using envsubst with this
syntax: `$ENVIRONMENT` or `''${VARIABLE}`.
This is useful to avoid putting secrets into the nix store.
'';
};
verbose = lib.mkOption {
type = lib.types.enum [
"CRITICAL"
"ERROR"
"WARNING"
"INFO"
"DEBUG"
];
default = "WARNING";
description = "Verbosity level.";
};
device = lib.mkOption {
type = lib.types.str;
description = "Device name to configure gateway to use.";
};
name = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
Name for the device.
Supported variables:
* `{ip}` IP address
* `{id}` The ID of the connected usb-dongle or core-module
`null` can be used for automatic detection from gateway firmware.
'';
};
mqtt = {
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Host where MQTT server is running.";
};
port = lib.mkOption {
type = lib.types.port;
default = 1883;
description = "Port of MQTT server.";
};
username = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "MQTT server access username.";
};
password = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "MQTT server access password.";
};
cafile = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Certificate Authority file for MQTT server access.";
};
certfile = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Certificate file for MQTT server access.";
};
keyfile = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Key file for MQTT server access.";
};
};
retainNodeMessages = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Specify that node messages should be retaied in MQTT broker.";
};
qosNodeMessages = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Set the guarantee of MQTT message delivery.";
};
baseTopicPrefix = lib.mkOption {
type = lib.types.str;
default = "";
description = "Topic prefix added to all MQTT messages.";
};
automaticRemoveKitFromNames = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Automatically remove kits.";
};
automaticRenameKitNodes = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Automatically rename kit's nodes.";
};
automaticRenameGenericNodes = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Automatically rename generic nodes.";
};
automaticRenameNodes = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Automatically rename all nodes.";
};
rename = lib.mkOption {
type = with lib.types; attrsOf str;
default = { };
description = "Rename nodes to different name.";
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
python3Packages.bcg
python3Packages.bch
];
systemd.services.bcg =
let
envConfig = cfg.environmentFiles != [ ];
finalConfig = if envConfig then "\${RUNTIME_DIRECTORY}/bcg.config.yaml" else configFile;
in
{
description = "BigClown Gateway";
wantedBy = [ "multi-user.target" ];
wants = [
"network-online.target"
]
++ lib.optional config.services.mosquitto.enable "mosquitto.service";
after = [ "network-online.target" ];
preStart = lib.mkIf envConfig ''
umask 077
${pkgs.envsubst}/bin/envsubst -i "${configFile}" -o "${finalConfig}"
'';
serviceConfig = {
EnvironmentFile = cfg.environmentFiles;
ExecStart = "${cfg.package}/bin/bcg -c ${finalConfig} -v ${cfg.verbose}";
RuntimeDirectory = "bcg";
};
};
};
}

View File

@@ -0,0 +1,65 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.beanstalkd;
pkg = pkgs.beanstalkd;
in
{
# interface
options = {
services.beanstalkd = {
enable = lib.mkEnableOption "the Beanstalk work queue";
listen = {
port = lib.mkOption {
type = lib.types.port;
description = "TCP port that will be used to accept client connections.";
default = 11300;
};
address = lib.mkOption {
type = lib.types.str;
description = "IP address to listen on.";
default = "127.0.0.1";
example = "0.0.0.0";
};
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open ports in the firewall for the server.";
};
};
};
# implementation
config = lib.mkIf cfg.enable {
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listen.port ];
};
environment.systemPackages = [ pkg ];
systemd.services.beanstalkd = {
description = "Beanstalk Work Queue";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
Restart = "always";
ExecStart = "${pkg}/bin/beanstalkd -l ${cfg.listen.address} -p ${toString cfg.listen.port} -b $STATE_DIRECTORY";
StateDirectory = "beanstalkd";
};
};
};
}

View File

@@ -0,0 +1,132 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.beesd;
logLevels = {
emerg = 0;
alert = 1;
crit = 2;
err = 3;
warning = 4;
notice = 5;
info = 6;
debug = 7;
};
fsOptions = with lib.types; {
options.spec = lib.mkOption {
type = str;
description = ''
Description of how to identify the filesystem to be duplicated by this
instance of bees. Note that deduplication crosses subvolumes; one must
not configure multiple instances for subvolumes of the same filesystem
(or block devices which are part of the same filesystem), but only for
completely independent btrfs filesystems.
This must be in a format usable by findmnt; that could be a key=value
pair, or a bare path to a mount point.
Using bare paths will allow systemd to start the beesd service only
after mounting the associated path.
'';
example = "LABEL=MyBulkDataDrive";
};
options.hashTableSizeMB = lib.mkOption {
type = lib.types.addCheck lib.types.int (n: lib.mod n 16 == 0);
default = 1024; # 1GB; default from upstream beesd script
description = ''
Hash table size in MB; must be a multiple of 16.
A larger ratio of index size to storage size means smaller blocks of
duplicate content are recognized.
If you have 1TB of data, a 4GB hash table (which is to say, a value of
4096) will permit 4KB extents (the smallest possible size) to be
recognized, whereas a value of 1024 -- creating a 1GB hash table --
will recognize only aligned duplicate blocks of 16KB.
'';
};
options.verbosity = lib.mkOption {
type = lib.types.enum (lib.attrNames logLevels ++ lib.attrValues logLevels);
apply = v: if lib.isString v then logLevels.${v} else v;
default = "info";
description = "Log verbosity (syslog keyword/level).";
};
options.workDir = lib.mkOption {
type = str;
default = ".beeshome";
description = ''
Name (relative to the root of the filesystem) of the subvolume where
the hash table will be stored.
'';
};
options.extraOptions = lib.mkOption {
type = listOf str;
default = [ ];
description = ''
Extra command-line options passed to the daemon. See upstream bees documentation.
'';
example = lib.literalExpression ''
[ "--thread-count" "4" ]
'';
};
};
in
{
options.services.beesd = {
filesystems = lib.mkOption {
type = with lib.types; attrsOf (submodule fsOptions);
description = "BTRFS filesystems to run block-level deduplication on.";
default = { };
example = lib.literalExpression ''
{
"-" = {
spec = "LABEL=root";
hashTableSizeMB = 2048;
verbosity = "crit";
extraOptions = [ "--loadavg-target" "5.0" ];
};
}
'';
};
};
config = lib.mkIf (cfg.filesystems != { }) {
systemd.packages = [ pkgs.bees ];
systemd.services = lib.mapAttrs' (
name: fs:
lib.nameValuePair "beesd@${name}" {
overrideStrategy = "asDropin";
serviceConfig = {
ExecStart =
let
configOpts = [
fs.spec
"verbosity=${toString fs.verbosity}"
"idxSizeMB=${toString fs.hashTableSizeMB}"
"workDir=${fs.workDir}"
];
configOptsStr = lib.escapeShellArgs configOpts;
in
[
""
"${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${lib.escapeShellArgs fs.extraOptions}"
];
SyslogIdentifier = "beesd"; # would otherwise be "bees-service-wrapper"
# Ensure that hashtable can be locked into memory
LimitMEMLOCK = "${toString fs.hashTableSizeMB}M";
MemoryMin = "${toString fs.hashTableSizeMB}M";
};
unitConfig.RequiresMountsFor = lib.mkIf (lib.hasPrefix "/" fs.spec) fs.spec;
wantedBy = [ "multi-user.target" ];
}
) cfg.filesystems;
};
}

View File

@@ -0,0 +1,200 @@
{
config,
lib,
pkgs,
...
}:
let
gunicorn = pkgs.python3Packages.gunicorn;
bepasty = pkgs.bepasty;
gevent = pkgs.python3Packages.gevent;
python = pkgs.python3Packages.python;
cfg = config.services.bepasty;
user = "bepasty";
group = "bepasty";
default_home = "/var/lib/bepasty";
in
{
options.services.bepasty = {
enable = lib.mkEnableOption "bepasty, a binary pastebin server";
servers = lib.mkOption {
default = { };
description = ''
configure a number of bepasty servers which will be started with
gunicorn.
'';
type =
with lib.types;
attrsOf (
submodule (
{ config, ... }:
{
options = {
bind = lib.mkOption {
type = lib.types.str;
description = ''
Bind address to be used for this server.
'';
example = "0.0.0.0:8000";
default = "127.0.0.1:8000";
};
dataDir = lib.mkOption {
type = lib.types.str;
description = ''
Path to the directory where the pastes will be saved to
'';
default = default_home + "/data";
};
defaultPermissions = lib.mkOption {
type = lib.types.str;
description = ''
default permissions for all unauthenticated accesses.
'';
example = "read,create,delete";
default = "read";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
description = ''
Extra configuration for bepasty server to be appended on the
configuration.
see <https://bepasty-server.readthedocs.org/en/latest/quickstart.html#configuring-bepasty>
for all options.
'';
default = "";
example = ''
PERMISSIONS = {
'myadminsecret': 'admin,list,create,read,delete',
}
MAX_ALLOWED_FILE_SIZE = 5 * 1000 * 1000
'';
};
secretKey = lib.mkOption {
type = lib.types.str;
description = ''
server secret for safe session cookies, must be set.
Warning: this secret is stored in the WORLD-READABLE Nix store!
It's recommended to use {option}`secretKeyFile`
which takes precedence over {option}`secretKey`.
'';
default = "";
};
secretKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
A file that contains the server secret for safe session cookies, must be set.
{option}`secretKeyFile` takes precedence over {option}`secretKey`.
Warning: when {option}`secretKey` is non-empty {option}`secretKeyFile`
defaults to a file in the WORLD-READABLE Nix store containing that secret.
'';
};
workDir = lib.mkOption {
type = lib.types.str;
description = ''
Path to the working directory (used for config and pidfile).
Defaults to the users home directory.
'';
default = default_home;
};
};
config = {
secretKeyFile = lib.mkDefault (
if config.secretKey != "" then
toString (
pkgs.writeTextFile {
name = "bepasty-secret-key";
text = config.secretKey;
}
)
else
null
);
};
}
)
);
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ bepasty ];
# creates gunicorn systemd service for each configured server
systemd.services = lib.mapAttrs' (
name: server:
lib.nameValuePair "bepasty-server-${name}-gunicorn" {
description = "Bepasty Server ${name}";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
restartIfChanged = true;
environment =
let
penv = python.buildEnv.override {
extraLibs = [
bepasty
gevent
];
};
in
{
BEPASTY_CONFIG = "${server.workDir}/bepasty-${name}.conf";
PYTHONPATH = "${penv}/${python.sitePackages}/";
};
serviceConfig = {
Type = "simple";
PrivateTmp = true;
ExecStartPre =
assert server.secretKeyFile != null;
pkgs.writeScript "bepasty-server.${name}-init" ''
#!/bin/sh
mkdir -p "${server.workDir}"
mkdir -p "${server.dataDir}"
chown ${user}:${group} "${server.workDir}" "${server.dataDir}"
cat > ${server.workDir}/bepasty-${name}.conf <<EOF
SITENAME="${name}"
STORAGE_FILESYSTEM_DIRECTORY="${server.dataDir}"
SECRET_KEY="$(cat "${server.secretKeyFile}")"
DEFAULT_PERMISSIONS="${server.defaultPermissions}"
${server.extraConfig}
EOF
'';
ExecStart = ''
${gunicorn}/bin/gunicorn bepasty.wsgi --name ${name} \
-u ${user} \
-g ${group} \
--workers 3 --log-level=info \
--bind=${server.bind} \
--pid ${server.workDir}/gunicorn-${name}.pid \
-k gevent
'';
};
}
) cfg.servers;
users.users.${user} = {
uid = config.ids.uids.bepasty;
group = group;
home = default_home;
};
users.groups.${group}.gid = config.ids.gids.bepasty;
};
}

View File

@@ -0,0 +1,149 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.blendfarm;
json = pkgs.formats.json { };
configFile = json.generate "ServerSettings" (defaultConfig // cfg.serverConfig);
defaultConfig = {
Port = 15000;
BroadcastPort = 16342;
BypassScriptUpdate = false;
BasicSecurityPassword = null;
};
in
{
meta.maintainers = with lib.maintainers; [ gador ];
options.services.blendfarm = with lib.types; {
enable = lib.mkEnableOption "Blendfarm, a render farm management software for Blender";
package = lib.mkPackageOption pkgs "blendfarm" { };
openFirewall = lib.mkEnableOption "allowing blendfarm network access through the firewall";
user = lib.mkOption {
description = "User under which blendfarm runs.";
default = "blendfarm";
type = str;
};
group = lib.mkOption {
description = "Group under which blendfarm runs.";
default = "blendfarm";
type = str;
};
basicSecurityPasswordFile = lib.mkOption {
description = ''
Path to the password file the client needs to connect to the server.
The password must not contain a forward slash.'';
default = null;
type = nullOr str;
};
blenderPackage = lib.mkPackageOption pkgs "blender" { };
serverConfig = lib.mkOption {
description = "Server configuration";
default = defaultConfig;
type = submodule {
freeformType = attrsOf anything;
options = {
Port = lib.mkOption {
description = "Default port blendfarm server listens on.";
default = 15000;
type = types.port;
};
BroadcastPort = lib.mkOption {
description = "Default port blendfarm server advertises itself on.";
default = 16342;
type = types.port;
};
BypassScriptUpdate = lib.mkOption {
description = "Prevents blendfarm from replacing the .py self-generated scripts.";
default = false;
type = bool;
};
};
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
networking.firewall = lib.optionalAttrs (cfg.openFirewall) {
allowedTCPPorts = [ cfg.serverConfig.Port ];
allowedUDPPorts = [ cfg.serverConfig.BroadcastPort ];
};
systemd.services.blendfarm-server = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
description = "blendfarm server";
path = [ cfg.blenderPackage ];
preStart = ''
rm -f ServerSettings
install -m640 ${configFile} ServerSettings
if [ ! -d "BlenderData/nix-blender-linux64" ]; then
mkdir -p BlenderData/nix-blender-linux64
echo "nix-blender" > VersionCustom
fi
rm -f BlenderData/nix-blender-linux64/blender
ln -s ${lib.getExe cfg.blenderPackage} BlenderData/nix-blender-linux64/blender
''
+ lib.optionalString (cfg.basicSecurityPasswordFile != null) ''
BLENDFARM_PASSWORD=$(${pkgs.systemd}/bin/systemd-creds cat BLENDFARM_PASS_FILE)
sed -i "s/null/\"$BLENDFARM_PASSWORD\"/g" ServerSettings
'';
serviceConfig = {
ExecStart = "${cfg.package}/bin/LogicReinc.BlendFarm.Server";
DynamicUser = true;
LogsDirectory = "blendfarm";
StateDirectory = "blendfarm";
WorkingDirectory = "/var/lib/blendfarm";
User = cfg.user;
Group = cfg.group;
StateDirectoryMode = "0755";
LoadCredential = lib.optional (
cfg.basicSecurityPasswordFile != null
) "BLENDFARM_PASS_FILE:${cfg.basicSecurityPasswordFile}";
ReadWritePaths = "";
CapabilityBoundingSet = "";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"@chown"
];
RestrictRealtime = true;
LockPersonality = true;
UMask = "0066";
ProtectHostname = true;
};
};
users.users.blendfarm = {
isSystemUser = true;
group = "blendfarm";
};
users.groups.blendfarm = { };
};
}

View File

@@ -0,0 +1,180 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.calibre-server;
documentationLink = "https://manual.calibre-ebook.com";
generatedDocumentationLink = documentationLink + "/generated/en/calibre-server.html";
execFlags = (
lib.concatStringsSep " " (
lib.mapAttrsToList (k: v: "${k} ${toString v}") (
lib.filterAttrs (name: value: value != null) {
"--listen-on" = cfg.host;
"--port" = cfg.port;
"--auth-mode" = cfg.auth.mode;
"--userdb" = cfg.auth.userDb;
}
)
++ [ (lib.optionalString (cfg.auth.enable == true) "--enable-auth") ]
++ cfg.extraFlags
)
);
in
{
imports = [
(lib.mkChangedOptionModule
[ "services" "calibre-server" "libraryDir" ]
[ "services" "calibre-server" "libraries" ]
(
config:
let
libraryDir = lib.getAttrFromPath [ "services" "calibre-server" "libraryDir" ] config;
in
[ libraryDir ]
)
)
];
options = {
services.calibre-server = {
enable = lib.mkEnableOption "calibre-server (e-book software)";
package = lib.mkPackageOption pkgs "calibre" { };
libraries = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ "/var/lib/calibre-server" ];
description = ''
Make sure each library path is initialized before service startup.
The directories of the libraries to serve. They must be readable for the user under which the server runs.
See the [calibredb documentation](${documentationLink}/generated/en/calibredb.html#add) for details.
'';
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Extra flags to pass to the calibre-server command.
See the [calibre-server documentation](${generatedDocumentationLink}) for details.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "calibre-server";
description = "The user under which calibre-server runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "calibre-server";
description = "The group under which calibre-server runs.";
};
host = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
example = "::1";
description = ''
The interface on which to listen for connections.
See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-listen-on) for details.
'';
};
port = lib.mkOption {
default = 8080;
type = lib.types.port;
description = ''
The port on which to listen for connections.
See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-port) for details.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the Calibre Server web interface.";
};
auth = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Password based authentication to access the server.
See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-enable-auth) for details.
'';
};
mode = lib.mkOption {
type = lib.types.enum [
"auto"
"basic"
"digest"
];
default = "auto";
description = ''
Choose the type of authentication used.
Set the HTTP authentication mode used by the server.
See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-auth-mode) for details.
'';
};
userDb = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = ''
Choose users database file to use for authentication.
Make sure users database file is initialized before service startup.
See the [calibre-server documentation](${documentationLink}/server.html#managing-user-accounts-from-the-command-line-only) for details.
'';
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.calibre-server = {
description = "Calibre Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Restart = "always";
ExecStart = "${cfg.package}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries} ${execFlags}";
};
};
environment.systemPackages = [ pkgs.calibre ];
users.users = lib.optionalAttrs (cfg.user == "calibre-server") {
calibre-server = {
home = "/var/lib/calibre-server";
createHome = true;
uid = config.ids.uids.calibre-server;
group = cfg.group;
};
};
users.groups = lib.optionalAttrs (cfg.group == "calibre-server") {
calibre-server = {
gid = config.ids.gids.calibre-server;
};
};
networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
};
meta.maintainers = with lib.maintainers; [ gaelreyrol ];
}

View File

@@ -0,0 +1,40 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.canto-daemon;
in
{
##### interface
options = {
services.canto-daemon = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the canto RSS daemon.";
};
};
};
##### implementation
config = lib.mkIf cfg.enable {
systemd.user.services.canto-daemon = {
description = "Canto RSS Daemon";
after = [ "network.target" ];
wantedBy = [ "default.target" ];
serviceConfig.ExecStart = "${pkgs.canto-daemon}/bin/canto-daemon";
};
};
}

View File

@@ -0,0 +1,87 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.cfdyndns;
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"cfdyndns"
"apikey"
] "Use services.cfdyndns.apikeyFile instead.")
];
options = {
services.cfdyndns = {
enable = lib.mkEnableOption "Cloudflare Dynamic DNS Client";
email = lib.mkOption {
type = lib.types.str;
description = ''
The email address to use to authenticate to CloudFlare.
'';
};
apiTokenFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The path to a file containing the API Token
used to authenticate with CloudFlare.
'';
};
apikeyFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The path to a file containing the API Key
used to authenticate with CloudFlare.
'';
};
records = lib.mkOption {
default = [ ];
example = [ "host.tld" ];
type = lib.types.listOf lib.types.str;
description = ''
The records to update in CloudFlare.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.cfdyndns = {
description = "CloudFlare Dynamic DNS Client";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
startAt = "*:0/5";
serviceConfig = {
Type = "simple";
LoadCredential = lib.optional (
cfg.apiTokenFile != null
) "CLOUDFLARE_APITOKEN_FILE:${cfg.apiTokenFile}";
DynamicUser = true;
};
environment = {
CLOUDFLARE_RECORDS = "${lib.concatStringsSep "," cfg.records}";
};
script = ''
${lib.optionalString (cfg.apikeyFile != null) ''
export CLOUDFLARE_APIKEY="$(cat ${lib.escapeShellArg cfg.apikeyFile})"
export CLOUDFLARE_EMAIL="${cfg.email}"
''}
${lib.optionalString (cfg.apiTokenFile != null) ''
export CLOUDFLARE_APITOKEN=$(${pkgs.systemd}/bin/systemd-creds cat CLOUDFLARE_APITOKEN_FILE)
''}
${pkgs.cfdyndns}/bin/cfdyndns
'';
};
};
}

View File

@@ -0,0 +1,153 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cgminer;
convType = v: if lib.isBool v then lib.boolToString v else toString v;
mergedHwConfig = lib.mapAttrsToList (
n: v: ''"${n}": "${(lib.concatStringsSep "," (map convType v))}"''
) (lib.foldAttrs (n: a: [ n ] ++ a) [ ] cfg.hardware);
mergedConfig =
lib.mapAttrsToList (
n: v: ''"${n}": ${if lib.isBool v then convType v else ''"${convType v}"''}''
) cfg.config;
cgminerConfig = pkgs.writeText "cgminer.conf" ''
{
${lib.concatStringsSep ",\n" mergedHwConfig},
${lib.concatStringsSep ",\n" mergedConfig},
"pools": [
${
lib.concatStringsSep ",\n" (
map (v: ''{"url": "${v.url}", "user": "${v.user}", "pass": "${v.pass}"}'') cfg.pools
)
}]
}
'';
in
{
###### interface
options = {
services.cgminer = {
enable = lib.mkEnableOption "cgminer, an ASIC/FPGA/GPU miner for bitcoin and litecoin";
package = lib.mkPackageOption pkgs "cgminer" { };
user = lib.mkOption {
type = lib.types.str;
default = "cgminer";
description = "User account under which cgminer runs";
};
pools = lib.mkOption {
default = [ ]; # Run benchmark
type = lib.types.listOf (lib.types.attrsOf lib.types.str);
description = "List of pools where to mine";
example = [
{
url = "http://p2pool.org:9332";
username = "17EUZxTvs9uRmPsjPZSYUU3zCz9iwstudk";
password = "X";
}
];
};
hardware = lib.mkOption {
default = [ ]; # Run without options
type = lib.types.listOf (lib.types.attrsOf (lib.types.either lib.types.str lib.types.int));
description = "List of config options for every GPU";
example = [
{
intensity = 9;
gpu-engine = "0-985";
gpu-fan = "0-85";
gpu-memclock = 860;
gpu-powertune = 20;
temp-cutoff = 95;
temp-overheat = 85;
temp-target = 75;
}
{
intensity = 9;
gpu-engine = "0-950";
gpu-fan = "0-85";
gpu-memclock = 825;
gpu-powertune = 20;
temp-cutoff = 95;
temp-overheat = 85;
temp-target = 75;
}
];
};
config = lib.mkOption {
default = { };
type = lib.types.attrsOf (lib.types.either lib.types.bool lib.types.int);
description = "Additional config";
example = {
auto-fan = true;
auto-gpu = true;
expiry = 120;
failover-only = true;
gpu-threads = 2;
log = 5;
queue = 1;
scan-time = 60;
temp-histeresys = 3;
};
};
};
};
###### implementation
config = lib.mkIf config.services.cgminer.enable {
users.users = lib.optionalAttrs (cfg.user == "cgminer") {
cgminer = {
isSystemUser = true;
group = "cgminer";
description = "Cgminer user";
};
};
users.groups = lib.optionalAttrs (cfg.user == "cgminer") {
cgminer = { };
};
environment.systemPackages = [ cfg.package ];
systemd.services.cgminer = {
path = [ pkgs.cgminer ];
after = [
"network.target"
"display-manager.service"
];
wantedBy = [ "multi-user.target" ];
environment = {
LD_LIBRARY_PATH = "/run/opengl-driver/lib:/run/opengl-driver-32/lib";
DISPLAY = ":${toString config.services.xserver.display}";
GPU_MAX_ALLOC_PERCENT = "100";
GPU_USE_SYNC_OBJECTS = "1";
};
startLimitIntervalSec = 60; # 1 min
serviceConfig = {
ExecStart = "${pkgs.cgminer}/bin/cgminer --syslog --text-only --config ${cgminerConfig}";
User = cfg.user;
RestartSec = "30s";
Restart = "always";
};
};
};
}

View File

@@ -0,0 +1,29 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.clipcat;
in
{
options.services.clipcat = {
enable = lib.mkEnableOption "Clipcat clipboard daemon";
package = lib.mkPackageOption pkgs "clipcat" { };
};
config = lib.mkIf cfg.enable {
systemd.user.services.clipcat = {
enable = true;
description = "clipcat daemon";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
serviceConfig.ExecStart = "${cfg.package}/bin/clipcatd --no-daemon";
};
environment.systemPackages = [ cfg.package ];
};
}

View File

@@ -0,0 +1,29 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.clipmenu;
in
{
options.services.clipmenu = {
enable = lib.mkEnableOption "clipmenu, the clipboard management daemon";
package = lib.mkPackageOption pkgs "clipmenu" { };
};
config = lib.mkIf cfg.enable {
systemd.user.services.clipmenu = {
enable = true;
description = "Clipboard management daemon";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
serviceConfig.ExecStart = "${cfg.package}/bin/clipmenud";
};
environment.systemPackages = [ cfg.package ];
};
}

View File

@@ -0,0 +1,96 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.confd;
confdConfig = ''
backend = "${cfg.backend}"
confdir = "${cfg.confDir}"
interval = ${toString cfg.interval}
nodes = [ ${lib.concatMapStringsSep "," (s: ''"${s}"'') cfg.nodes}, ]
prefix = "${cfg.prefix}"
log-level = "${cfg.logLevel}"
watch = ${lib.boolToString cfg.watch}
'';
in
{
options.services.confd = {
enable = lib.mkEnableOption "confd, a service to manage local application configuration files using templates and data from etcd/consul/redis/zookeeper";
backend = lib.mkOption {
description = "Confd config storage backend to use.";
default = "etcd";
type = lib.types.enum [
"etcd"
"consul"
"redis"
"zookeeper"
];
};
interval = lib.mkOption {
description = "Confd check interval.";
default = 10;
type = lib.types.int;
};
nodes = lib.mkOption {
description = "Confd list of nodes to connect to.";
default = [ "http://127.0.0.1:2379" ];
type = lib.types.listOf lib.types.str;
};
watch = lib.mkOption {
description = "Confd, whether to watch etcd config for changes.";
default = true;
type = lib.types.bool;
};
prefix = lib.mkOption {
description = "The string to prefix to keys.";
default = "/";
type = lib.types.path;
};
logLevel = lib.mkOption {
description = "Confd log level.";
default = "info";
type = lib.types.enum [
"info"
"debug"
];
};
confDir = lib.mkOption {
description = "The path to the confd configs.";
default = "/etc/confd";
type = lib.types.path;
};
package = lib.mkPackageOption pkgs "confd" { };
};
config = lib.mkIf cfg.enable {
systemd.services.confd = {
description = "Confd Service.";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/confd";
};
};
environment.etc = {
"confd/confd.toml".text = confdConfig;
};
environment.systemPackages = [ cfg.package ];
services.etcd.enable = lib.mkIf (cfg.backend == "etcd") (lib.mkDefault true);
};
}

View File

@@ -0,0 +1,89 @@
{
config,
lib,
pkgs,
...
}:
{
options = {
services.conman = {
enable = lib.mkEnableOption ''
Enable the conman Console manager.
Either `configFile` or `config` must be specified.
'';
package = lib.mkPackageOption pkgs "conman" { };
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/conman.conf";
description = ''
The absolute path to the configuration file.
Either `configFile` or `config` must be specified.
See <https://github.com/dun/conman/wiki/Man-5-conman.conf#files>.
'';
};
config = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
example = ''
server coredump=off
server keepalive=on
server loopback=off
server timestamp=1h
# global config
global log="/var/log/conman/%N.log"
global seropts="9600,8n1"
global ipmiopts="U:<user>,P:<password>"
'';
description = ''
The configuration object.
Either `configFile` or `config` must be specified.
See <https://github.com/dun/conman/wiki/Man-5-conman.conf#files>.
'';
};
};
};
meta.maintainers = with lib.maintainers; [
frantathefranta
];
config =
let
cfg = config.services.conman;
configFile =
if cfg.configFile != null then
cfg.configFile
else
pkgs.writeTextFile {
name = "conman.conf";
text = cfg.config;
};
in
lib.mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.configFile != null) && (cfg.config == null) || (cfg.configFile == null && cfg.config != null);
message = "Either but not both `configFile` and `config` must be specified for conman.";
}
];
environment.systemPackages = [ cfg.package ];
systemd.services.conmand = {
description = "serial console management program";
documentation = [ "man:conman(8)" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/conmand -F -c ${configFile}";
KillMode = "process";
};
};
};
}

View File

@@ -0,0 +1,68 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cpuminer-cryptonight;
json = builtins.toJSON (
cfg
// {
enable = null;
threads = if cfg.threads == 0 then null else toString cfg.threads;
}
);
confFile = builtins.toFile "cpuminer.json" json;
in
{
options = {
services.cpuminer-cryptonight = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the cpuminer cryptonight miner.
'';
};
url = lib.mkOption {
type = lib.types.str;
description = "URL of mining server";
};
user = lib.mkOption {
type = lib.types.str;
description = "Username for mining server";
};
pass = lib.mkOption {
type = lib.types.str;
default = "x";
description = "Password for mining server";
};
threads = lib.mkOption {
type = lib.types.ints.unsigned;
default = 0;
description = "Number of miner threads, defaults to available processors";
};
};
};
config = lib.mkIf config.services.cpuminer-cryptonight.enable {
systemd.services.cpuminer-cryptonight = {
description = "Cryptonight cpuminer";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${pkgs.cpuminer-multi}/bin/minerd --syslog --config=${confFile}";
User = "nobody";
};
};
};
}

View File

@@ -0,0 +1,205 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
mkOption
types
mkIf
mkMerge
mkDefault
mkEnableOption
mkPackageOption
maintainers
;
cfg = config.services.db-rest;
in
{
options = {
services.db-rest = {
enable = mkEnableOption "db-rest service";
user = mkOption {
type = types.str;
default = "db-rest";
description = "User account under which db-rest runs.";
};
group = mkOption {
type = types.str;
default = "db-rest";
description = "Group under which db-rest runs.";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The host address the db-rest server should listen on.";
};
port = mkOption {
type = types.port;
default = 3000;
description = "The port the db-rest server should listen on.";
};
redis = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable caching with redis for db-rest.";
};
createLocally = mkOption {
type = types.bool;
default = true;
description = "Configure a local redis server for db-rest.";
};
host = mkOption {
type = with types; nullOr str;
default = null;
description = "Redis host.";
};
port = mkOption {
type = with types; nullOr port;
default = null;
description = "Redis port.";
};
user = mkOption {
type = with types; nullOr str;
default = null;
description = "Optional username used for authentication with redis.";
};
passwordFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/run/keys/db-rest/pasword-redis-db";
description = "Path to a file containing the redis password.";
};
useSSL = mkOption {
type = types.bool;
default = true;
description = "Use SSL if using a redis network connection.";
};
};
package = mkPackageOption pkgs "db-rest" { };
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.redis.enable && !cfg.redis.createLocally)
-> (cfg.redis.host != null && cfg.redis.port != null);
message = ''
{option}`services.db-rest.redis.createLocally` and redis network connection ({option}`services.db-rest.redis.host` or {option}`services.db-rest.redis.port`) enabled. Disable either of them.
'';
}
{
assertion = (cfg.redis.enable && !cfg.redis.createLocally) -> (cfg.redis.passwordFile != null);
message = ''
{option}`services.db-rest.redis.createLocally` is disabled, but {option}`services.db-rest.redis.passwordFile` is not set.
'';
}
];
systemd.services.db-rest = mkMerge [
{
description = "db-rest service";
after = [ "network.target" ] ++ lib.optional cfg.redis.createLocally "redis-db-rest.service";
requires = lib.optional cfg.redis.createLocally "redis-db-rest.service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
Restart = "always";
RestartSec = 5;
WorkingDirectory = cfg.package;
User = cfg.user;
Group = cfg.group;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
MemoryDenyWriteExecute = false;
LoadCredential = lib.optional (
cfg.redis.enable && cfg.redis.passwordFile != null
) "REDIS_PASSWORD:${cfg.redis.passwordFile}";
ExecStart = mkDefault "${cfg.package}/bin/db-rest";
RemoveIPC = true;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectClock = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
PrivateMounts = true;
SystemCallArchitectures = "native";
ProtectHostname = true;
LockPersonality = true;
ProtectKernelTunables = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RestrictNamespaces = true;
ProtectSystem = "strict";
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectHome = true;
PrivateUsers = true;
PrivateTmp = true;
CapabilityBoundingSet = "";
};
environment = {
NODE_ENV = "production";
NODE_EXTRA_CA_CERTS = config.security.pki.caBundle;
HOSTNAME = cfg.host;
PORT = toString cfg.port;
};
}
(mkIf cfg.redis.enable (
if cfg.redis.createLocally then
{ environment.REDIS_URL = config.services.redis.servers.db-rest.unixSocket; }
else
{
script =
let
username = lib.optionalString (cfg.redis.user != null) (cfg.redis.user);
host = cfg.redis.host;
port = toString cfg.redis.port;
protocol = if cfg.redis.useSSL then "rediss" else "redis";
in
''
export REDIS_URL="${protocol}://${username}:$(${config.systemd.package}/bin/systemd-creds cat REDIS_PASSWORD)@${host}:${port}"
exec ${cfg.package}/bin/db-rest
'';
}
))
];
users.users = lib.mkMerge [
(lib.mkIf (cfg.user == "db-rest") {
db-rest = {
isSystemUser = true;
group = cfg.group;
};
})
(lib.mkIf cfg.redis.createLocally { ${cfg.user}.extraGroups = [ "redis-db-rest" ]; })
];
users.groups = lib.mkIf (cfg.group == "db-rest") { db-rest = { }; };
services.redis.servers.db-rest.enable = cfg.redis.enable && cfg.redis.createLocally;
};
meta.maintainers = with maintainers; [ marie ];
}

View File

@@ -0,0 +1,33 @@
{
pkgs,
config,
lib,
...
}:
let
cfg = config.services.devmon;
in
{
options = {
services.devmon = {
enable = lib.mkEnableOption "devmon, an automatic device mounting daemon";
};
};
config = lib.mkIf cfg.enable {
systemd.user.services.devmon = {
description = "devmon automatic device mounting daemon";
wantedBy = [ "default.target" ];
path = [
pkgs.udevil
pkgs.procps
pkgs.udisks2
pkgs.which
];
serviceConfig.ExecStart = "${pkgs.udevil}/bin/devmon";
};
services.udisks2.enable = true;
};
}

View File

@@ -0,0 +1,129 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.devpi-server;
secretsFileName = "devpi-secret-file";
stateDirName = "devpi";
runtimeDir = "/run/${stateDirName}";
serverDir = "/var/lib/${stateDirName}";
in
{
options.services.devpi-server = {
enable = lib.mkEnableOption "Devpi Server";
package = lib.mkPackageOption pkgs "devpi-server" { };
primaryUrl = lib.mkOption {
type = lib.types.str;
description = "Url for the primary node. Required option for replica nodes.";
};
replica = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run node as a replica.
Requires the secretFile option and the primaryUrl to be enabled.
'';
};
secretFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a shared secret file used for synchronization,
Required for all nodes in a replica/primary setup.
'';
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
domain/ip address to listen on
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 3141;
description = "The port on which Devpi Server will listen.";
};
openFirewall = lib.mkEnableOption "opening the default ports in the firewall for Devpi Server";
};
config = lib.mkIf cfg.enable {
systemd.services.devpi-server = {
enable = true;
description = "devpi PyPI-compatible server";
documentation = [ "https://devpi.net/docs/devpi/devpi/stable/+d/index.html" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
# Since at least devpi-server 6.10.0, devpi requires the secrets file to
# have 0600 permissions.
preStart = ''
${lib.optionalString (
!isNull cfg.secretFile
) "install -Dm 0600 \${CREDENTIALS_DIRECTORY}/devpi-secret ${runtimeDir}/${secretsFileName}"}
if [ -f ${serverDir}/.nodeinfo ]; then
# already initialized the package index, exit gracefully
exit 0
fi
${cfg.package}/bin/devpi-init --serverdir ${serverDir} ''
+ lib.optionalString cfg.replica "--role=replica --master-url=${cfg.primaryUrl}";
serviceConfig = {
LoadCredential = lib.mkIf (!isNull cfg.secretFile) [
"devpi-secret:${cfg.secretFile}"
];
Restart = "always";
ExecStart =
let
args = [
"--request-timeout=5"
"--serverdir=${serverDir}"
"--host=${cfg.host}"
"--port=${builtins.toString cfg.port}"
]
++ lib.optionals (!isNull cfg.secretFile) [
"--secretfile=${runtimeDir}/${secretsFileName}"
]
++ (
if cfg.replica then
[
"--role=replica"
"--master-url=${cfg.primaryUrl}"
]
else
[ "--role=master" ]
);
in
"${cfg.package}/bin/devpi-server ${lib.concatStringsSep " " args}";
DynamicUser = true;
StateDirectory = stateDirName;
RuntimeDirectory = stateDirName;
PrivateDevices = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
meta.maintainers = [ lib.maintainers.cafkafk ];
}

View File

@@ -0,0 +1,84 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dictd;
in
{
###### interface
options = {
services.dictd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the DICT.org dictionary server.
'';
};
DBs = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = with pkgs.dictdDBs; [
wiktionary
wordnet
];
defaultText = lib.literalExpression "with pkgs.dictdDBs; [ wiktionary wordnet ]";
example = lib.literalExpression "[ pkgs.dictdDBs.nld2eng ]";
description = "List of databases to make available.";
};
};
};
###### implementation
config =
let
dictdb = pkgs.dictDBCollector {
dictlist = map (x: {
name = x.name;
filename = x;
}) cfg.DBs;
};
in
lib.mkIf cfg.enable {
# get the command line client on system path to make some use of the service
environment.systemPackages = [ pkgs.dict ];
environment.etc."dict.conf".text = ''
server localhost
'';
users.users.dictd = {
group = "dictd";
description = "DICT.org dictd server";
home = "${dictdb}/share/dictd";
uid = config.ids.uids.dictd;
};
users.groups.dictd.gid = config.ids.gids.dictd;
systemd.services.dictd = {
description = "DICT.org Dictionary Server";
wantedBy = [ "multi-user.target" ];
environment = {
LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
};
# Work around the fact that dictd doesn't handle SIGTERM; it terminates
# with code 143 instead of exiting with code 0.
serviceConfig.SuccessExitStatus = [ 143 ];
serviceConfig.Type = "forking";
script = "${pkgs.dict}/sbin/dictd -s -c ${dictdb}/share/dictd/dictd.conf --locale en_US.UTF-8";
};
};
}

View File

@@ -0,0 +1,112 @@
# Disnix server
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.disnix;
in
{
###### interface
options = {
services.disnix = {
enable = lib.mkEnableOption "Disnix";
enableMultiUser = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to support multi-user mode by enabling the Disnix D-Bus service";
};
useWebServiceInterface = lib.mkEnableOption "the DisnixWebService interface running on Apache Tomcat";
package = lib.mkPackageOption pkgs "disnix" { };
enableProfilePath = lib.mkEnableOption "exposing the Disnix profiles in the system's PATH";
profiles = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "default" ];
description = "Names of the Disnix profiles to expose in the system's PATH";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
services.dysnomia.enable = true;
environment.systemPackages = [
pkgs.disnix
]
++ lib.optional cfg.useWebServiceInterface pkgs.DisnixWebService;
environment.variables.PATH = lib.optionals cfg.enableProfilePath (
map (profileName: "/nix/var/nix/profiles/disnix/${profileName}/bin") cfg.profiles
);
environment.variables.DISNIX_REMOTE_CLIENT = lib.optionalString (cfg.enableMultiUser) "disnix-client";
services.dbus.enable = true;
services.dbus.packages = [ pkgs.disnix ];
services.tomcat.enable = cfg.useWebServiceInterface;
services.tomcat.extraGroups = [ "disnix" ];
services.tomcat.javaOpts = "${lib.optionalString cfg.useWebServiceInterface "-Djava.library.path=${pkgs.libmatthew_java}/lib/jni"} ";
services.tomcat.sharedLibs =
lib.optional cfg.useWebServiceInterface "${pkgs.DisnixWebService}/share/java/DisnixConnection.jar"
++ lib.optional cfg.useWebServiceInterface "${pkgs.dbus_java}/share/java/dbus.jar";
services.tomcat.webapps = lib.optional cfg.useWebServiceInterface pkgs.DisnixWebService;
users.groups.disnix.gid = config.ids.gids.disnix;
systemd.services = {
disnix = lib.mkIf cfg.enableMultiUser {
description = "Disnix server";
wants = [ "dysnomia.target" ];
wantedBy = [ "multi-user.target" ];
after = [
"dbus.service"
]
++ lib.optional config.services.httpd.enable "httpd.service"
++ lib.optional config.services.mysql.enable "mysql.service"
++ lib.optional config.services.postgresql.enable "postgresql.target"
++ lib.optional config.services.tomcat.enable "tomcat.service"
++ lib.optional config.services.svnserve.enable "svnserve.service"
++ lib.optional config.services.mongodb.enable "mongodb.service"
++ lib.optional config.services.influxdb.enable "influxdb.service";
restartIfChanged = false;
path = [
config.nix.package
cfg.package
config.services.dysnomia.package
"/run/current-system/sw"
];
environment = {
HOME = "/root";
}
// (lib.optionalAttrs (config.environment.variables ? DYSNOMIA_CONTAINERS_PATH) {
inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH;
})
// (lib.optionalAttrs (config.environment.variables ? DYSNOMIA_MODULES_PATH) {
inherit (config.environment.variables) DYSNOMIA_MODULES_PATH;
});
serviceConfig.ExecStart = "${cfg.package}/bin/disnix-service";
};
};
};
}

View File

@@ -0,0 +1,188 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dockerRegistry;
blobCache = if cfg.enableRedisCache then "redis" else "inmemory";
registryConfig = {
version = "0.1";
log.fields.service = "registry";
storage = {
cache.blobdescriptor = blobCache;
delete.enabled = cfg.enableDelete;
}
// (lib.optionalAttrs (cfg.storagePath != null) { filesystem.rootdirectory = cfg.storagePath; });
http = {
addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
headers.X-Content-Type-Options = [ "nosniff" ];
};
health.storagedriver = {
enabled = true;
interval = "10s";
threshold = 3;
};
};
registryConfig.redis = lib.mkIf cfg.enableRedisCache {
addr = "${cfg.redisUrl}";
password = "${cfg.redisPassword}";
db = 0;
dialtimeout = "10ms";
readtimeout = "10ms";
writetimeout = "10ms";
pool = {
maxidle = 16;
maxactive = 64;
idletimeout = "300s";
};
};
configFile = cfg.configFile;
in
{
options.services.dockerRegistry = {
enable = lib.mkEnableOption "Docker Registry";
package = lib.mkPackageOption pkgs "docker-distribution" {
example = "gitlab-container-registry";
};
listenAddress = lib.mkOption {
description = "Docker registry host or ip to bind to.";
default = "127.0.0.1";
type = lib.types.str;
};
port = lib.mkOption {
description = "Docker registry port to bind to.";
default = 5000;
type = lib.types.port;
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Opens the port used by the firewall.";
};
storagePath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = "/var/lib/docker-registry";
description = ''
Docker registry storage path for the filesystem storage backend. Set to
null to configure another backend via extraConfig.
'';
};
enableDelete = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable delete for manifests and blobs.";
};
enableRedisCache = lib.mkEnableOption "redis as blob cache";
redisUrl = lib.mkOption {
type = lib.types.str;
default = "localhost:6379";
description = "Set redis host and port.";
};
redisPassword = lib.mkOption {
type = lib.types.str;
default = "";
description = "Set redis password.";
};
extraConfig = lib.mkOption {
description = ''
Docker extra registry configuration.
'';
example = lib.literalExpression ''
{
log.level = "debug";
}
'';
default = { };
type = lib.types.attrs;
};
configFile = lib.mkOption {
default = pkgs.writeText "docker-registry-config.yml" (
builtins.toJSON (lib.recursiveUpdate registryConfig cfg.extraConfig)
);
defaultText = lib.literalExpression ''pkgs.writeText "docker-registry-config.yml" "# my custom docker-registry-config.yml ..."'';
description = ''
Path to CNCF distribution config file.
Setting this option will override any configuration applied by the extraConfig option.
'';
type = lib.types.path;
};
enableGarbageCollect = lib.mkEnableOption "garbage collect";
garbageCollectDates = lib.mkOption {
default = "daily";
type = lib.types.str;
description = ''
Specification (in the format described by
{manpage}`systemd.time(7)`) of the time at
which the garbage collect will occur.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.docker-registry = {
description = "Docker Container Registry";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
script = ''
${cfg.package}/bin/registry serve ${configFile}
'';
serviceConfig = {
User = "docker-registry";
WorkingDirectory = cfg.storagePath;
AmbientCapabilities = lib.mkIf (cfg.port < 1024) "cap_net_bind_service";
};
};
systemd.services.docker-registry-garbage-collect = {
description = "Run Garbage Collection for docker registry";
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig.Type = "oneshot";
script = ''
${cfg.package}/bin/registry garbage-collect ${configFile}
/run/current-system/systemd/bin/systemctl restart docker-registry.service
'';
startAt = lib.optional cfg.enableGarbageCollect cfg.garbageCollectDates;
};
users.users.docker-registry =
(lib.optionalAttrs (cfg.storagePath != null) {
createHome = true;
home = cfg.storagePath;
})
// {
group = "docker-registry";
isSystemUser = true;
};
users.groups.docker-registry = { };
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

View File

@@ -0,0 +1,131 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
cfg = config.services.docling-serve;
in
{
options = {
services.docling-serve = {
enable = lib.mkEnableOption "Docling Serve server";
package = lib.mkPackageOption pkgs "docling-serve" { };
stateDir = lib.mkOption {
type = types.path;
default = "/var/lib/docling-serve";
example = "/home/foo";
description = "State directory of Docling Serve.";
};
host = lib.mkOption {
type = types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = ''
The host address which the Docling Serve server HTTP interface listens to.
'';
};
port = lib.mkOption {
type = types.port;
default = 5001;
example = 11111;
description = ''
Which port the Docling Serve server listens to.
'';
};
environment = lib.mkOption {
type = types.attrsOf types.str;
default = {
DOCLING_SERVE_ENABLE_UI = "False";
};
example = ''
{
DOCLING_SERVE_ENABLE_UI = "True";
}
'';
description = ''
Extra environment variables for Docling Serve.
For more details see <https://github.com/docling-project/docling-serve/blob/main/docs/configuration.md>
'';
};
environmentFile = lib.mkOption {
description = ''
Environment file to be passed to the systemd service.
Useful for passing secrets to the service to prevent them from being
world-readable in the Nix store.
'';
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/secrets/doclingServeSecrets";
};
openFirewall = lib.mkOption {
type = types.bool;
default = false;
description = ''
Whether to open the firewall for Docling Serve.
This adds `services.Docling Serve.port` to `networking.firewall.allowedTCPPorts`.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.docling-serve = {
description = "Running Docling as an API service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
HF_HOME = ".";
EASYOCR_MODULE_PATH = ".";
MPLCONFIGDIR = ".";
}
// cfg.environment;
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} run --host \"${cfg.host}\" --port ${toString cfg.port}";
EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
WorkingDirectory = cfg.stateDir;
StateDirectory = "docling-serve";
RuntimeDirectory = "docling-serve";
RuntimeDirectoryMode = "0755";
PrivateTmp = true;
DynamicUser = true;
DevicePolicy = "closed";
LockPersonality = true;
PrivateUsers = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
UMask = "0077";
CapabilityBoundingSet = "";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
ProtectClock = true;
ProtectProc = "invisible";
};
};
networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,55 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.domoticz;
pkgDesc = "Domoticz home automation";
in
{
options = {
services.domoticz = {
enable = lib.mkEnableOption pkgDesc;
bind = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "IP address to bind to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port to bind to for HTTP, set to 0 to disable HTTP.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services."domoticz" = {
description = pkgDesc;
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "domoticz";
Restart = "always";
ExecStart = ''
${pkgs.domoticz}/bin/domoticz -noupdates -www ${toString cfg.port} -wwwbind ${cfg.bind} -sslwww 0 -userdata /var/lib/domoticz -approot ${pkgs.domoticz}/share/domoticz/ -pidfile /var/run/domoticz.pid
'';
};
};
};
}

View File

@@ -0,0 +1,126 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.duckdns;
duckdns = pkgs.writeShellScriptBin "duckdns" ''
DRESPONSE=$(curl -sS --max-time 60 --no-progress-meter -k -K- <<< "url = \"https://www.duckdns.org/update?verbose=true&domains=$DUCKDNS_DOMAINS&token=$DUCKDNS_TOKEN&ip=\"")
IPV4=$(echo "$DRESPONSE" | awk 'NR==2')
IPV6=$(echo "$DRESPONSE" | awk 'NR==3')
RESPONSE=$(echo "$DRESPONSE" | awk 'NR==1')
IPCHANGE=$(echo "$DRESPONSE" | awk 'NR==4')
if [[ "$RESPONSE" = "OK" ]] && [[ "$IPCHANGE" = "UPDATED" ]]; then
if [[ "$IPV4" != "" ]] && [[ "$IPV6" == "" ]]; then
echo "Your IP was updated at $(date) to IPv4: $IPV4"
elif [[ "$IPV4" == "" ]] && [[ "$IPV6" != "" ]]; then
echo "Your IP was updated at $(date) to IPv6: $IPV6"
else
echo "Your IP was updated at $(date) to IPv4: $IPV4 & IPv6 to: $IPV6"
fi
elif [[ "$RESPONSE" = "OK" ]] && [[ "$IPCHANGE" = "NOCHANGE" ]]; then
echo "DuckDNS request at $(date) successful. IP(s) unchanged."
else
echo -e "Something went wrong, please check your settings\nThe response returned was:\n$DRESPONSE\n"
exit 1
fi
'';
in
{
options.services.duckdns = {
enable = lib.mkEnableOption "DuckDNS Dynamic DNS Client";
tokenFile = lib.mkOption {
default = null;
type = lib.types.path;
description = ''
The path to a file containing the token
used to authenticate with DuckDNS.
'';
};
domains = lib.mkOption {
default = null;
type = lib.types.nullOr (lib.types.listOf lib.types.str);
example = [ "examplehost" ];
description = ''
The domain(s) to update in DuckDNS
(without the .duckdns.org suffix)
'';
};
domainsFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
example = lib.literalExpression ''
pkgs.writeText "duckdns-domains.txt" '''
examplehost
examplehost2
examplehost3
'''
'';
description = ''
The path to a file containing a
newline-separated list of DuckDNS
domain(s) to be updated
(without the .duckdns.org suffix)
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.domains != null || cfg.domainsFile != null;
message = "Either services.duckdns.domains or services.duckdns.domainsFile has to be defined";
}
{
assertion = !(cfg.domains != null && cfg.domainsFile != null);
message = "services.duckdns.domains and services.duckdns.domainsFile can't both be defined at the same time";
}
{
assertion = (cfg.tokenFile != null);
message = "services.duckdns.tokenFile has to be defined";
}
];
environment.systemPackages = [ duckdns ];
systemd.services.duckdns = {
description = "DuckDNS Dynamic DNS Client";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
startAt = "*:0/5";
path = [
pkgs.gnused
pkgs.systemd
pkgs.curl
pkgs.gawk
duckdns
];
serviceConfig = {
Type = "simple";
LoadCredential = [
"DUCKDNS_TOKEN_FILE:${cfg.tokenFile}"
]
++ lib.optionals (cfg.domainsFile != null) [ "DUCKDNS_DOMAINS_FILE:${cfg.domainsFile}" ];
DynamicUser = true;
};
script = ''
export DUCKDNS_TOKEN=$(systemd-creds cat DUCKDNS_TOKEN_FILE)
${lib.optionalString (cfg.domains != null) ''
export DUCKDNS_DOMAINS='${lib.strings.concatStringsSep "," cfg.domains}'
''}
${lib.optionalString (cfg.domainsFile != null) ''
export DUCKDNS_DOMAINS=$(systemd-creds cat DUCKDNS_DOMAINS_FILE | sed -z 's/\n/,/g')
''}
exec ${lib.getExe duckdns}
'';
};
};
meta.maintainers = with lib.maintainers; [ notthebee ];
}

View File

@@ -0,0 +1,42 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.duckling;
in
{
options = {
services.duckling = {
enable = lib.mkEnableOption "duckling";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = ''
Port on which duckling will run.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.duckling = {
description = "Duckling server service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
PORT = builtins.toString cfg.port;
};
serviceConfig = {
ExecStart = "${pkgs.haskellPackages.duckling}/bin/duckling-example-exe --no-access-log --no-error-log";
Restart = "always";
DynamicUser = true;
};
};
};
}

View File

@@ -0,0 +1,26 @@
# Dump1090-fa {#module-services-dump1090-fa}
[dump1090-fa](https://github.com/flightaware/dump1090) is a demodulator and decoder for ADS-B, Mode S, and Mode 3A/3C aircraft transponder messages. It can receive and decode these messages from an attached software-defined radio or from data received over a network connection.
## Configuration {#module-services-dump1090-fa-configuration}
When enabled, this module automatically creates a systemd service to start the `dump1090-fa` application. The application will then write its JSON output files to `/run/dump1090-fa`.
Exposing the integrated web interface is left to the user's configuration. Below is a minimal example demonstrating how to serve it using Nginx:
```nix
{ pkgs, ... }:
{
services.dump1090-fa.enable = true;
services.nginx = {
enable = true;
virtualHosts."dump1090-fa" = {
locations = {
"/".alias = "${pkgs.dump1090-fa}/share/dump1090/";
"/data/".alias = "/run/dump1090-fa/";
};
};
};
}
```

View File

@@ -0,0 +1,135 @@
{
pkgs,
config,
lib,
...
}:
let
cfg = config.services.dump1090-fa;
inherit (lib) mkOption types;
in
{
options.services.dump1090-fa = {
enable = lib.mkEnableOption "dump1090-fa";
package = lib.mkPackageOption pkgs "dump1090-fa" { };
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Additional passed arguments";
};
};
config = lib.mkIf cfg.enable {
systemd.services.dump1090-fa = {
description = "dump1090 ADS-B receiver (FlightAware customization)";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = lib.escapeShellArgs (
[
(lib.getExe cfg.package)
"--net"
"--write-json"
"%t/dump1090-fa"
]
++ cfg.extraArgs
);
DynamicUser = true;
SupplementaryGroups = "plugdev";
RuntimeDirectory = "dump1090-fa";
WorkingDirectory = "%t/dump1090-fa";
RuntimeDirectoryMode = 755;
PrivateNetwork = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectSystem = "strict";
ProtectHostname = true;
RestrictSUIDSGID = true;
RestrictNamespaces =
"~"
+ (lib.concatStringsSep " " [
"cgroup"
"ipc"
"net"
"mnt"
"pid"
"user"
"uts"
]);
CapabilityBoundingSet = [
"~CAP_AUDIT_CONTROL"
"~CAP_AUDIT_READ"
"~CAP_AUDIT_WRITE"
"~CAP_KILL"
"~CAP_MKNOD"
"~CAP_NET_BIND_SERVICE"
"~CAP_NET_BROADCAST"
"~CAP_NET_ADMIN"
"~CAP_NET_RAW"
"~CAP_SYS_RAWIO"
"~CAP_SYS_MODULE"
"~CAP_SYS_PTRACE"
"~CAP_SYS_TIME"
"~CAP_SYS_NICE"
"~CAP_SYS_RESOURCE"
"~CAP_CHOWN"
"~CAP_FSETID"
"~CAP_SETUID"
"~CAP_SETGID"
"~CAP_SETPCAP"
"~CAP_SETFCAP"
"~CAP_DAC_OVERRIDE"
"~CAP_DAC_READ_SEARCH"
"~CAP_FOWNER"
"~CAP_IPC_OWNER"
"~CAP_IPC_LOCK"
"~CAP_SYS_BOOT"
"~CAP_SYS_ADMIN"
"~CAP_MAC_ADMIN"
"~CAP_MAC_OVERRIDE"
"~CAP_SYS_CHROOT"
"~CAP_BLOCK_SUSPEND"
"~CAP_WAKE_ALARM"
"~CAP_LEASE"
"~CAP_SYS_PACCT"
];
SystemCallFilter = [
"~@clock"
"~@debug"
"~@module"
"~@mount"
"~@raw-io"
"~@reboot"
"~@swap"
"~@privileged"
"~@resources"
"~@cpu-emulation"
"~@obsolete"
];
RestrictAddressFamilies = [ "~AF_PACKET" ];
ProtectControlGroups = true;
UMask = "0022";
SystemCallArchitectures = "native";
};
};
};
meta = {
maintainers = with lib.maintainers; [ aciceri ];
doc = ./dump1090-fa.md;
};
}

View File

@@ -0,0 +1,86 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dwm-status;
format = pkgs.formats.toml { };
configFile = format.generate "dwm-status.toml" cfg.settings;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "dwm-status" "order" ]
[ "services" "dwm-status" "settings" "order" ]
)
(lib.mkRemovedOptionModule [
"services"
"dwm-status"
"extraConfig"
] "Use services.dwm-status.settings instead.")
];
options = {
services.dwm-status = {
enable = lib.mkEnableOption "dwm-status user service";
package = lib.mkPackageOption pkgs "dwm-status" {
example = "dwm-status.override { enableAlsaUtils = false; }";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options.order = lib.mkOption {
type = lib.types.listOf (
lib.types.enum [
"audio"
"backlight"
"battery"
"cpu_load"
"network"
"time"
]
);
default = [ ];
description = ''
List of enabled features in order.
'';
};
};
default = { };
example = {
order = [
"battery"
"cpu_load"
"time"
];
time = {
format = "%F %a %r";
update_seconds = true;
};
};
description = ''
Config options for dwm-status, see <https://github.com/Gerschtli/dwm-status#configuration>
for available options.
'';
};
};
};
config = lib.mkIf cfg.enable {
services.upower.enable = lib.mkIf (lib.elem "battery" cfg.settings.order) true;
systemd.user.services.dwm-status = {
description = "Highly performant and configurable DWM status service";
wantedBy = [ "graphical-session.target" ];
partOf = [ "graphical-session.target" ];
serviceConfig.ExecStart = "${cfg.package}/bin/dwm-status ${configFile} --quiet";
};
};
}

View File

@@ -0,0 +1,302 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.dysnomia;
printProperties =
properties:
lib.concatMapStrings (
propertyName:
let
property = properties.${propertyName};
in
if lib.isList property then
"${propertyName}=(${
lib.concatMapStrings (elem: "\"${toString elem}\" ") (properties.${propertyName})
})\n"
else
"${propertyName}=\"${toString property}\"\n"
) (builtins.attrNames properties);
properties = pkgs.stdenv.mkDerivation {
name = "dysnomia-properties";
buildCommand = ''
cat > $out << "EOF"
${printProperties cfg.properties}
EOF
'';
};
containersDir = pkgs.stdenv.mkDerivation {
name = "dysnomia-containers";
buildCommand = ''
mkdir -p $out
cd $out
${lib.concatMapStrings (
containerName:
let
containerProperties = cfg.containers.${containerName};
in
''
cat > ${containerName} <<EOF
${printProperties containerProperties}
type=${containerName}
EOF
''
) (builtins.attrNames cfg.containers)}
'';
};
linkMutableComponents =
{ containerName }:
''
mkdir ${containerName}
${lib.concatMapStrings (
componentName:
let
component = cfg.components.${containerName}.${componentName};
in
"ln -s ${component} ${containerName}/${componentName}\n"
) (builtins.attrNames (cfg.components.${containerName} or { }))}
'';
componentsDir = pkgs.stdenv.mkDerivation {
name = "dysnomia-components";
buildCommand = ''
mkdir -p $out
cd $out
${lib.concatMapStrings (containerName: linkMutableComponents { inherit containerName; }) (
builtins.attrNames cfg.components
)}
'';
};
dysnomiaFlags = {
enableApacheWebApplication = config.services.httpd.enable;
enableAxis2WebService = config.services.tomcat.axis2.enable;
enableDockerContainer = config.virtualisation.docker.enable;
enableEjabberdDump = config.services.ejabberd.enable;
enableMySQLDatabase = config.services.mysql.enable;
enablePostgreSQLDatabase = config.services.postgresql.enable;
enableTomcatWebApplication = config.services.tomcat.enable;
enableMongoDatabase = config.services.mongodb.enable;
enableSubversionRepository = config.services.svnserve.enable;
enableInfluxDatabase = config.services.influxdb.enable;
};
in
{
options = {
services.dysnomia = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable Dysnomia";
};
enableAuthentication = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to publish privacy-sensitive authentication credentials";
};
package = lib.mkOption {
type = lib.types.path;
description = "The Dysnomia package";
};
properties = lib.mkOption {
description = "An attribute set in which each attribute represents a machine property. Optionally, these values can be shell substitutions.";
default = { };
type = lib.types.attrs;
};
containers = lib.mkOption {
description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
default = { };
type = lib.types.attrsOf lib.types.attrs;
};
components = lib.mkOption {
description = "An attribute set in which each key represents a container and each value an attribute set in which each key represents a component and each value a derivation constructing its initial state";
default = { };
type = lib.types.attrsOf lib.types.attrs;
};
extraContainerProperties = lib.mkOption {
description = "An attribute set providing additional container settings in addition to the default properties";
default = { };
type = lib.types.attrs;
};
extraContainerPaths = lib.mkOption {
description = "A list of paths containing additional container configurations that are added to the search folders";
default = [ ];
type = lib.types.listOf lib.types.path;
};
extraModulePaths = lib.mkOption {
description = "A list of paths containing additional modules that are added to the search folders";
default = [ ];
type = lib.types.listOf lib.types.path;
};
enableLegacyModules = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable Dysnomia legacy process and wrapper modules";
};
};
};
imports = [
(lib.mkRenamedOptionModule [ "dysnomia" ] [ "services" "dysnomia" ])
];
config = lib.mkIf cfg.enable {
environment.etc = {
"dysnomia/containers" = {
source = containersDir;
};
"dysnomia/components" = {
source = componentsDir;
};
"dysnomia/properties" = {
source = properties;
};
};
environment.variables = {
DYSNOMIA_STATEDIR = "/var/state/dysnomia-nixos";
DYSNOMIA_CONTAINERS_PATH = "${
lib.concatMapStrings (containerPath: "${containerPath}:") cfg.extraContainerPaths
}/etc/dysnomia/containers";
DYSNOMIA_MODULES_PATH = "${
lib.concatMapStrings (modulePath: "${modulePath}:") cfg.extraModulePaths
}/etc/dysnomia/modules";
};
environment.systemPackages = [ cfg.package ];
services.dysnomia.package = pkgs.dysnomia.override (
origArgs:
dysnomiaFlags
// lib.optionalAttrs (cfg.enableLegacyModules) {
enableLegacy = builtins.trace ''
WARNING: Dysnomia has been configured to use the legacy 'process' and 'wrapper'
modules for compatibility reasons! If you rely on these modules, consider
migrating to better alternatives.
More information: <https://raw.githubusercontent.com/svanderburg/dysnomia/f65a9a84827bcc4024d6b16527098b33b02e4054/README-legacy.md>
If you have migrated already or don't rely on these Dysnomia modules, you can
disable legacy mode with the following NixOS configuration option:
dysnomia.enableLegacyModules = false;
In a future version of Dysnomia (and NixOS) the legacy option will go away!
'' true;
}
);
services.dysnomia.properties = {
hostname = config.networking.hostName;
inherit (pkgs.stdenv.hostPlatform) system;
supportedTypes = [
"echo"
"fileset"
"process"
"wrapper"
# These are not base modules, but they are still enabled because they work with technology that are always enabled in NixOS
"systemd-unit"
"sysvinit-script"
"nixos-configuration"
]
++ lib.optional (dysnomiaFlags.enableApacheWebApplication) "apache-webapplication"
++ lib.optional (dysnomiaFlags.enableAxis2WebService) "axis2-webservice"
++ lib.optional (dysnomiaFlags.enableDockerContainer) "docker-container"
++ lib.optional (dysnomiaFlags.enableEjabberdDump) "ejabberd-dump"
++ lib.optional (dysnomiaFlags.enableInfluxDatabase) "influx-database"
++ lib.optional (dysnomiaFlags.enableMySQLDatabase) "mysql-database"
++ lib.optional (dysnomiaFlags.enablePostgreSQLDatabase) "postgresql-database"
++ lib.optional (dysnomiaFlags.enableTomcatWebApplication) "tomcat-webapplication"
++ lib.optional (dysnomiaFlags.enableMongoDatabase) "mongo-database"
++ lib.optional (dysnomiaFlags.enableSubversionRepository) "subversion-repository";
};
services.dysnomia.containers = lib.recursiveUpdate (
{
process = { };
wrapper = { };
}
// lib.optionalAttrs (config.services.httpd.enable) {
apache-webapplication = {
documentRoot = config.services.httpd.virtualHosts.localhost.documentRoot;
};
}
// lib.optionalAttrs (config.services.tomcat.axis2.enable) { axis2-webservice = { }; }
// lib.optionalAttrs (config.services.ejabberd.enable) {
ejabberd-dump = {
ejabberdUser = config.services.ejabberd.user;
};
}
// lib.optionalAttrs (config.services.mysql.enable) {
mysql-database = {
mysqlPort = config.services.mysql.settings.mysqld.port;
mysqlSocket = "/run/mysqld/mysqld.sock";
}
// lib.optionalAttrs cfg.enableAuthentication {
mysqlUsername = "root";
};
}
// lib.optionalAttrs (config.services.postgresql.enable) {
postgresql-database = {
}
// lib.optionalAttrs (cfg.enableAuthentication) {
postgresqlUsername = "postgres";
};
}
// lib.optionalAttrs (config.services.tomcat.enable) {
tomcat-webapplication = {
tomcatPort = 8080;
};
}
// lib.optionalAttrs (config.services.mongodb.enable) { mongo-database = { }; }
// lib.optionalAttrs (config.services.influxdb.enable) {
influx-database = {
influxdbUsername = config.services.influxdb.user;
influxdbDataDir = "${config.services.influxdb.dataDir}/data";
influxdbMetaDir = "${config.services.influxdb.dataDir}/meta";
};
}
// lib.optionalAttrs (config.services.svnserve.enable) {
subversion-repository = {
svnBaseDir = config.services.svnserve.svnBaseDir;
};
}
) cfg.extraContainerProperties;
boot.extraSystemdUnitPaths = [ "/etc/systemd-mutable/system" ];
system.activationScripts.dysnomia = ''
mkdir -p /etc/systemd-mutable/system
if [ ! -f /etc/systemd-mutable/system/dysnomia.target ]
then
( echo "[Unit]"
echo "Description=Services that are activated and deactivated by Dysnomia"
echo "After=final.target"
) > /etc/systemd-mutable/system/dysnomia.target
fi
'';
};
}

View File

@@ -0,0 +1,117 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.errbot;
pluginEnv =
plugins:
pkgs.buildEnv {
name = "errbot-plugins";
paths = plugins;
};
mkConfigDir =
instanceCfg: dataDir:
pkgs.writeTextDir "config.py" ''
import logging
BACKEND = '${instanceCfg.backend}'
BOT_DATA_DIR = '${dataDir}'
BOT_EXTRA_PLUGIN_DIR = '${pluginEnv instanceCfg.plugins}'
BOT_LOG_LEVEL = logging.${instanceCfg.logLevel}
BOT_LOG_FILE = False
BOT_ADMINS = (${lib.concatMapStringsSep "," (name: "'${name}'") instanceCfg.admins})
BOT_IDENTITY = ${builtins.toJSON instanceCfg.identity}
${instanceCfg.extraConfig}
'';
in
{
options = {
services.errbot.instances = lib.mkOption {
default = { };
description = "Errbot instance configs";
type = lib.types.attrsOf (
lib.types.submodule {
options = {
dataDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Data directory for errbot instance.";
};
plugins = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "List of errbot plugin derivations.";
};
logLevel = lib.mkOption {
type = lib.types.str;
default = "INFO";
description = "Errbot log level";
};
admins = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of identifiers of errbot admins.";
};
backend = lib.mkOption {
type = lib.types.str;
default = "XMPP";
description = "Errbot backend name.";
};
identity = lib.mkOption {
type = lib.types.attrs;
description = "Errbot identity configuration";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "String to be appended to the config verbatim";
};
};
}
);
};
};
config = lib.mkIf (cfg.instances != { }) {
users.users.errbot = {
group = "errbot";
isSystemUser = true;
};
users.groups.errbot = { };
systemd.services = lib.mapAttrs' (
name: instanceCfg:
lib.nameValuePair "errbot-${name}" (
let
dataDir = if instanceCfg.dataDir != null then instanceCfg.dataDir else "/var/lib/errbot/${name}";
in
{
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p ${dataDir}
chown -R errbot:errbot ${dataDir}
'';
serviceConfig = {
User = "errbot";
Restart = "on-failure";
ExecStart = "${pkgs.errbot}/bin/errbot -c ${mkConfigDir instanceCfg dataDir}/config.py";
PermissionsStartOnly = true;
};
}
)
) cfg.instances;
};
}

View File

@@ -0,0 +1,250 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.etebase-server;
iniFmt = pkgs.formats.ini { };
configIni = iniFmt.generate "etebase-server.ini" cfg.settings;
defaultUser = "etebase-server";
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"etebase-server"
"customIni"
] "Set the option `services.etebase-server.settings' instead.")
(lib.mkRemovedOptionModule [
"services"
"etebase-server"
"database"
] "Set the option `services.etebase-server.settings.database' instead.")
(lib.mkRenamedOptionModule
[ "services" "etebase-server" "secretFile" ]
[ "services" "etebase-server" "settings" "secret_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "etebase-server" "host" ]
[ "services" "etebase-server" "settings" "allowed_hosts" "allowed_host1" ]
)
];
options = {
services.etebase-server = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Whether to enable the Etebase server.
Once enabled you need to create an admin user by invoking the
shell command `etebase-server createsuperuser` with
the user specified by the `user` option or a superuser.
Then you can login and create accounts on your-etebase-server.com/admin
'';
};
package = lib.mkPackageOption pkgs "etebase-server" { };
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/etebase-server";
description = "Directory to store the Etebase server data.";
};
port = lib.mkOption {
type = with lib.types; nullOr port;
default = 8001;
description = "Port to listen on.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open ports in the firewall for the server.
'';
};
unixSocket = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "The path to the socket to bind to.";
example = "/run/etebase-server/etebase-server.sock";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = iniFmt.type;
options = {
global = {
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to set django's DEBUG flag.
'';
};
secret_file = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
The path to a file containing the secret
used as django's SECRET_KEY.
'';
};
static_root = lib.mkOption {
type = lib.types.str;
default = "${cfg.dataDir}/static";
defaultText = lib.literalExpression ''"''${config.services.etebase-server.dataDir}/static"'';
description = "The directory for static files.";
};
media_root = lib.mkOption {
type = lib.types.str;
default = "${cfg.dataDir}/media";
defaultText = lib.literalExpression ''"''${config.services.etebase-server.dataDir}/media"'';
description = "The media directory.";
};
};
allowed_hosts = {
allowed_host1 = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
example = "localhost";
description = ''
The main host that is allowed access.
'';
};
};
database = {
engine = lib.mkOption {
type = lib.types.enum [
"django.db.backends.sqlite3"
"django.db.backends.postgresql"
];
default = "django.db.backends.sqlite3";
description = "The database engine to use.";
};
name = lib.mkOption {
type = lib.types.str;
default = "${cfg.dataDir}/db.sqlite3";
defaultText = lib.literalExpression ''"''${config.services.etebase-server.dataDir}/db.sqlite3"'';
description = "The database name.";
};
};
};
};
default = { };
description = ''
Configuration for `etebase-server`. Refer to
<https://github.com/etesync/server/blob/master/etebase-server.ini.example>
and <https://github.com/etesync/server/wiki>
for details on supported values.
'';
example = {
global = {
debug = true;
media_root = "/path/to/media";
};
allowed_hosts = {
allowed_host2 = "localhost";
};
};
};
user = lib.mkOption {
type = lib.types.str;
default = defaultUser;
description = "User under which Etebase server runs.";
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
(runCommand "etebase-server"
{
nativeBuildInputs = [ makeWrapper ];
}
''
makeWrapper ${cfg.package}/bin/etebase-server \
$out/bin/etebase-server \
--chdir ${lib.escapeShellArg cfg.dataDir} \
--prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
''
)
];
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
]
++ lib.optionals (cfg.unixSocket != null) [
"d '${builtins.dirOf cfg.unixSocket}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
];
systemd.services.etebase-server = {
description = "An Etebase (EteSync 2.0) server";
after = [
"network.target"
"systemd-tmpfiles-setup.service"
];
path = [ cfg.package ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Restart = "always";
WorkingDirectory = cfg.dataDir;
};
environment = {
ETEBASE_EASY_CONFIG_PATH = configIni;
PYTHONPATH = cfg.package.pythonPath;
};
preStart = ''
# Auto-migrate on first run or if the package has changed
versionFile="${cfg.dataDir}/src-version"
if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
etebase-server migrate --no-input
etebase-server collectstatic --no-input --clear
echo ${cfg.package} > "$versionFile"
fi
'';
script =
let
python = cfg.package.python;
networking =
if cfg.unixSocket != null then
"--uds ${cfg.unixSocket}"
else
"--host 0.0.0.0 --port ${toString cfg.port}";
in
''
${python.pkgs.uvicorn}/bin/uvicorn ${networking} \
--app-dir ${cfg.package}/${cfg.package.python.sitePackages} \
etebase_server.asgi:application
'';
};
users = lib.optionalAttrs (cfg.user == defaultUser) {
users.${defaultUser} = {
isSystemUser = true;
group = defaultUser;
home = cfg.dataDir;
};
groups.${defaultUser} = { };
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

View File

@@ -0,0 +1,95 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.etesync-dav;
in
{
options.services.etesync-dav = {
enable = lib.mkEnableOption "etesync-dav, end-to-end encrypted sync for contacts, calendars and tasks";
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "The server host address.";
};
port = lib.mkOption {
type = lib.types.port;
default = 37358;
description = "The server host port.";
};
apiUrl = lib.mkOption {
type = lib.types.str;
default = "https://api.etebase.com/partner/etesync/";
description = "The url to the etesync API.";
};
openFirewall = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Whether to open the firewall for the specified port.";
};
sslCertificate = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/etesync.crt";
description = ''
Path to server SSL certificate. It will be copied into
etesync-dav's data directory.
'';
};
sslCertificateKey = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/etesync.key";
description = ''
Path to server SSL certificate key. It will be copied into
etesync-dav's data directory.
'';
};
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
systemd.services.etesync-dav = {
description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.etesync-dav ];
environment = {
ETESYNC_LISTEN_ADDRESS = cfg.host;
ETESYNC_LISTEN_PORT = toString cfg.port;
ETESYNC_URL = cfg.apiUrl;
ETESYNC_DATA_DIR = "/var/lib/etesync-dav";
};
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = "etesync-dav";
ExecStart = "${pkgs.etesync-dav}/bin/etesync-dav";
ExecStartPre = lib.mkIf (cfg.sslCertificate != null || cfg.sslCertificateKey != null) (
pkgs.writers.writeBash "etesync-dav-copy-keys" ''
${lib.optionalString (cfg.sslCertificate != null) ''
cp ${toString cfg.sslCertificate} $STATE_DIRECTORY/etesync.crt
''}
${lib.optionalString (cfg.sslCertificateKey != null) ''
cp ${toString cfg.sslCertificateKey} $STATE_DIRECTORY/etesync.key
''}
''
);
Restart = "on-failure";
RestartSec = "30min 1s";
};
};
};
}

View File

@@ -0,0 +1,62 @@
{
config,
lib,
pkgs,
...
}:
let
format = pkgs.formats.yaml { };
cfg = config.services.evdevremapkeys;
in
{
options.services.evdevremapkeys = {
enable = lib.mkEnableOption ''evdevremapkeys, a daemon to remap events on linux input devices'';
settings = lib.mkOption {
type = format.type;
default = { };
description = ''
config.yaml for evdevremapkeys
'';
};
};
config = lib.mkIf cfg.enable {
boot.kernelModules = [ "uinput" ];
services.udev.extraRules = ''
KERNEL=="uinput", MODE="0660", GROUP="input"
'';
users.groups.evdevremapkeys = { };
users.users.evdevremapkeys = {
description = "evdevremapkeys service user";
group = "evdevremapkeys";
extraGroups = [ "input" ];
isSystemUser = true;
};
systemd.services.evdevremapkeys = {
description = "evdevremapkeys";
wantedBy = [ "multi-user.target" ];
serviceConfig =
let
config = format.generate "config.yaml" cfg.settings;
in
{
ExecStart = "${pkgs.evdevremapkeys}/bin/evdevremapkeys --config-file ${config}";
User = "evdevremapkeys";
Group = "evdevremapkeys";
StateDirectory = "evdevremapkeys";
Restart = "always";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateNetwork = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelTunables = true;
ProtectSystem = true;
};
};
};
}

View File

@@ -0,0 +1,181 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.evremap;
format = pkgs.formats.toml { };
settings = lib.attrsets.filterAttrs (n: v: v != null) cfg.settings;
configFile = format.generate "evremap.toml" settings;
key = lib.types.strMatching "(BTN|KEY)_[[:upper:][:digit:]_]+" // {
description = "key ID prefixed with BTN_ or KEY_";
};
mkKeyOption =
description:
lib.mkOption {
type = key;
description = ''
${description}
You can get a list of keys by running `evremap list-keys`.
'';
};
mkKeySeqOption =
description:
(mkKeyOption description)
// {
type = lib.types.listOf key;
};
dualRoleModule = lib.types.submodule {
options = {
input = mkKeyOption "The key that should be remapped.";
hold = mkKeySeqOption "The key sequence that should be output when the input key is held.";
tap = mkKeySeqOption "The key sequence that should be output when the input key is tapped.";
};
};
remapModule = lib.types.submodule {
options = {
input = mkKeySeqOption "The key sequence that should be remapped.";
output = mkKeySeqOption "The key sequence that should be output when the input sequence is entered.";
};
};
in
{
options.services.evremap = {
enable = lib.mkEnableOption "evremap, a keyboard input remapper for Linux/Wayland systems";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options = {
device_name = lib.mkOption {
type = lib.types.str;
example = "AT Translated Set 2 keyboard";
description = ''
The name of the device that should be remapped.
You can get a list of devices by running `evremap list-devices` with elevated permissions.
'';
};
phys = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "usb-0000:07:00.3-2.1.1/input0";
description = ''
The physical device name to listen on.
This attribute may be specified to disambiguate multiple devices with the same device name.
The physical device names of each device can be obtained by running `evremap list-devices` with elevated permissions.
'';
};
dual_role = lib.mkOption {
type = lib.types.listOf dualRoleModule;
default = [ ];
example = [
{
input = "KEY_CAPSLOCK";
hold = [ "KEY_LEFTCTRL" ];
tap = [ "KEY_ESC" ];
}
];
description = ''
List of dual-role remappings that output different key sequences based on whether the
input key is held or tapped.
'';
};
remap = lib.mkOption {
type = lib.types.listOf remapModule;
default = [ ];
example = [
{
input = [
"KEY_LEFTALT"
"KEY_UP"
];
output = [ "KEY_PAGEUP" ];
}
];
description = ''
List of remappings.
'';
};
};
};
description = ''
Settings for evremap.
See the [upstream documentation](https://github.com/wez/evremap/blob/master/README.md#configuration)
for how to configure evremap.
'';
default = { };
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.evremap ];
hardware.uinput.enable = true;
systemd.services.evremap = {
description = "evremap - keyboard input remapper";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${lib.getExe pkgs.evremap} remap ${configFile}";
DynamicUser = true;
User = "evremap";
SupplementaryGroups = [
config.users.groups.input.name
config.users.groups.uinput.name
];
Restart = "on-failure";
RestartSec = 5;
TimeoutSec = 20;
# Hardening
ProtectClock = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectHostname = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectHome = true;
ProcSubset = "pid";
PrivateTmp = true;
PrivateNetwork = true;
PrivateUsers = true;
RestrictRealtime = true;
RestrictNamespaces = true;
RestrictAddressFamilies = "none";
MemoryDenyWriteExecute = true;
LockPersonality = true;
IPAddressDeny = "any";
AmbientCapabilities = "";
CapabilityBoundingSet = "";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
"~@privileged"
];
UMask = "0027";
};
};
};
}

View File

@@ -0,0 +1,105 @@
# Felix server
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.felix;
in
{
###### interface
options = {
services.felix = {
enable = lib.mkEnableOption "the Apache Felix OSGi service";
bundles = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ pkgs.felix_remoteshell ];
defaultText = lib.literalExpression "[ pkgs.felix_remoteshell ]";
description = "List of bundles that should be activated on startup";
};
user = lib.mkOption {
type = lib.types.str;
default = "osgi";
description = "User account under which Apache Felix runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "osgi";
description = "Group account under which Apache Felix runs.";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.groups.osgi.gid = config.ids.gids.osgi;
users.users.osgi = {
uid = config.ids.uids.osgi;
description = "OSGi user";
home = "/homeless-shelter";
};
systemd.services.felix = {
description = "Felix server";
wantedBy = [ "multi-user.target" ];
preStart = ''
# Initialise felix instance on first startup
if [ ! -d /var/felix ]
then
# Symlink system files
mkdir -p /var/felix
chown ${cfg.user}:${cfg.group} /var/felix
for i in ${pkgs.felix}/*
do
if [ "$i" != "${pkgs.felix}/bundle" ]
then
ln -sfn $i /var/felix/$(basename $i)
fi
done
# Symlink bundles
mkdir -p /var/felix/bundle
chown ${cfg.user}:${cfg.group} /var/felix/bundle
for i in ${pkgs.felix}/bundle/* ${toString cfg.bundles}
do
if [ -f $i ]
then
ln -sfn $i /var/felix/bundle/$(basename $i)
elif [ -d $i ]
then
for j in $i/bundle/*
do
ln -sfn $j /var/felix/bundle/$(basename $j)
done
fi
done
fi
'';
script = ''
cd /var/felix
${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${cfg.user} -c '${pkgs.jre}/bin/java -jar bin/felix.jar'
'';
};
};
}

View File

@@ -0,0 +1,58 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.flaresolverr;
in
{
options = {
services.flaresolverr = {
enable = lib.mkEnableOption "FlareSolverr, a proxy server to bypass Cloudflare protection";
package = lib.mkPackageOption pkgs "flaresolverr" { };
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open the port in the firewall for FlareSolverr.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8191;
description = "The port on which FlareSolverr will listen for incoming HTTP traffic.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.flaresolverr = {
description = "FlareSolverr";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HOME = "/run/flaresolverr";
PORT = toString cfg.port;
};
serviceConfig = {
SyslogIdentifier = "flaresolverr";
Restart = "always";
RestartSec = 5;
Type = "simple";
DynamicUser = true;
RuntimeDirectory = "flaresolverr";
WorkingDirectory = "/run/flaresolverr";
ExecStart = lib.getExe cfg.package;
TimeoutStopSec = 30;
};
};
networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
};
}

View File

@@ -0,0 +1,119 @@
# Forgejo {#module-forgejo}
Forgejo is a soft-fork of gitea, with strong community focus, as well
as on self-hosting and federation. [Codeberg](https://codeberg.org) is
deployed from it.
See [upstream docs](https://forgejo.org/docs/latest/).
The method of choice for running forgejo is using [`services.forgejo`](#opt-services.forgejo.enable).
::: {.warning}
Running forgejo using `services.gitea.package = pkgs.forgejo` is no longer
recommended.
If you experience issues with your instance using `services.gitea`,
**DO NOT** report them to the `services.gitea` module maintainers.
**DO** report them to the `services.forgejo` module maintainers instead.
:::
## Migration from Gitea {#module-forgejo-migration-gitea}
::: {.note}
Migrating is, while not strictly necessary at this point, highly recommended.
Both modules and projects are likely to diverge further with each release.
Which might lead to an even more involved migration.
:::
::: {.warning}
The last supported version of Forgejo which supports migration from Gitea is
*10.0.x*. You should *NOT* try to migrate from Gitea to Forgejo `11.x` or
higher without first migrating to `10.0.x`.
See [upstream migration guide](https://forgejo.org/docs/latest/admin/gitea-migration/)
The last supported version of *Gitea* for this migration process is *1.22*. Do
*NOT* try to directly migrate from Gitea *1.23* or higher, as it will likely
result in data loss.
See [upstream news article](https://forgejo.org/2024-12-gitea-compatibility/)
:::
In order to migrate, the version of Forgejo needs to be pinned to `10.0.x`
*before* using the latest version. This means that nixpkgs commit
[`3bb45b041e7147e2fd2daf689e26a1f970a55d65`](https://github.com/NixOS/nixpkgs/commit/3bb45b041e7147e2fd2daf689e26a1f970a55d65)
or earlier should be used.
To do this, temporarily add the following to your `configuration.nix`:
```nix
{ pkgs, ... }:
let
nixpkgs-forgejo-10 = import (pkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
rev = "3bb45b041e7147e2fd2daf689e26a1f970a55d65";
hash = "sha256-8JL5NI9eUcGzzbR/ARkrG81WLwndoxqI650mA/4rUGI=";
}) { };
in
{
services.forgejo.package = nixpkgs-forgejo-10.forgejo;
}
```
### Full-Migration {#module-forgejo-migration-gitea-default}
This will migrate the state directory (data), rename and chown the database and
delete the gitea user.
::: {.note}
This will also change the git remote ssh-url user from `gitea@` to `forgejo@`,
when using the host's openssh server (default) instead of the integrated one.
:::
Instructions for PostgreSQL (default). Adapt accordingly for other databases:
```sh
systemctl stop gitea
mv /var/lib/gitea /var/lib/forgejo
runuser -u postgres -- psql -c '
ALTER USER gitea RENAME TO forgejo;
ALTER DATABASE gitea RENAME TO forgejo;
'
nixos-rebuild switch
systemctl stop forgejo
chown -R forgejo:forgejo /var/lib/forgejo
systemctl restart forgejo
```
Afterwards, the Forgejo version can be set back to a newer desired version.
### Alternatively, keeping the gitea user {#module-forgejo-migration-gitea-impersonate}
Alternatively, instead of renaming the database, copying the state folder and
changing the user, the forgejo module can be set up to re-use the old storage
locations and database, instead of having to copy or rename them.
Make sure to disable `services.gitea`, when doing this.
```nix
{
services.gitea.enable = false;
services.forgejo = {
enable = true;
user = "gitea";
group = "gitea";
stateDir = "/var/lib/gitea";
database.name = "gitea";
database.user = "gitea";
};
users.users.gitea = {
home = "/var/lib/gitea";
useDefaultShell = true;
group = "gitea";
isSystemUser = true;
};
users.groups.gitea = { };
}
```

View File

@@ -0,0 +1,844 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.forgejo;
opt = options.services.forgejo;
format = pkgs.formats.ini { };
exe = lib.getExe cfg.package;
pg = config.services.postgresql;
useMysql = cfg.database.type == "mysql";
usePostgresql = cfg.database.type == "postgres";
useSqlite = cfg.database.type == "sqlite3";
secrets =
let
mkSecret =
section: values:
lib.mapAttrsToList (key: value: {
env = envEscape "FORGEJO__${section}__${key}__FILE";
path = value;
}) values;
# https://codeberg.org/forgejo/forgejo/src/tag/v7.0.2/contrib/environment-to-ini/environment-to-ini.go
envEscape =
string: lib.replaceStrings [ "." "-" ] [ "_0X2E_" "_0X2D_" ] (lib.strings.toUpper string);
in
lib.flatten (lib.mapAttrsToList mkSecret cfg.secrets);
inherit (lib)
literalExpression
mkChangedOptionModule
mkDefault
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
mkRemovedOptionModule
mkRenamedOptionModule
optionalAttrs
optionals
optionalString
types
;
in
{
imports = [
(mkRenamedOptionModule
[ "services" "forgejo" "appName" ]
[ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ]
)
(mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ]
"services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead"
)
(mkRemovedOptionModule [ "services" "forgejo" "database" "password" ]
"services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead"
)
(mkRenamedOptionModule
[ "services" "forgejo" "mailerPasswordFile" ]
[ "services" "forgejo" "secrets" "mailer" "PASSWD" ]
)
# copied from services.gitea; remove at some point
(mkRenamedOptionModule
[ "services" "forgejo" "cookieSecure" ]
[ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "disableRegistration" ]
[ "services" "forgejo" "settings" "service" "DISABLE_REGISTRATION" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "domain" ]
[ "services" "forgejo" "settings" "server" "DOMAIN" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "httpAddress" ]
[ "services" "forgejo" "settings" "server" "HTTP_ADDR" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "httpPort" ]
[ "services" "forgejo" "settings" "server" "HTTP_PORT" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "log" "level" ]
[ "services" "forgejo" "settings" "log" "LEVEL" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "log" "rootPath" ]
[ "services" "forgejo" "settings" "log" "ROOT_PATH" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "rootUrl" ]
[ "services" "forgejo" "settings" "server" "ROOT_URL" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "ssh" "clonePort" ]
[ "services" "forgejo" "settings" "server" "SSH_PORT" ]
)
(mkRenamedOptionModule
[ "services" "forgejo" "staticRootPath" ]
[ "services" "forgejo" "settings" "server" "STATIC_ROOT_PATH" ]
)
(mkChangedOptionModule
[ "services" "forgejo" "enableUnixSocket" ]
[ "services" "forgejo" "settings" "server" "PROTOCOL" ]
(config: if config.services.forgejo.enableUnixSocket then "http+unix" else "http")
)
(mkRemovedOptionModule [ "services" "forgejo" "ssh" "enable" ]
"services.forgejo.ssh.enable has been migrated into freeform setting services.forgejo.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted"
)
];
options = {
services.forgejo = {
enable = mkEnableOption "Forgejo, a software forge";
package = mkPackageOption pkgs "forgejo-lts" { };
useWizard = mkOption {
default = false;
type = types.bool;
description = ''
Whether to use the built-in installation wizard instead of
declaratively managing the {file}`app.ini` config file in nix.
'';
};
stateDir = mkOption {
default = "/var/lib/forgejo";
type = types.str;
description = "Forgejo data directory.";
};
customDir = mkOption {
default = "${cfg.stateDir}/custom";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"'';
type = types.str;
description = ''
Base directory for custom templates and other options.
If {option}`${opt.useWizard}` is disabled (default), this directory will also
hold secrets and the resulting {file}`app.ini` config at runtime.
'';
};
user = mkOption {
type = types.str;
default = "forgejo";
description = "User account under which Forgejo runs.";
};
group = mkOption {
type = types.str;
default = "forgejo";
description = "Group under which Forgejo runs.";
};
database = {
type = mkOption {
type = types.enum [
"sqlite3"
"mysql"
"postgres"
];
example = "mysql";
default = "sqlite3";
description = "Database engine to use.";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Database host address.";
};
port = mkOption {
type = types.port;
default = if usePostgresql then pg.settings.port else 3306;
defaultText = literalExpression ''
if config.${opt.database.type} != "postgresql"
then 3306
else 5432
'';
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "forgejo";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "forgejo";
description = "Database user.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/forgejo-dbpassword";
description = ''
A file containing the password corresponding to
{option}`${opt.database.user}`.
'';
};
socket = mkOption {
type = types.nullOr types.path;
default =
if (cfg.database.createDatabase && usePostgresql) then
"/run/postgresql"
else if (cfg.database.createDatabase && useMysql) then
"/run/mysqld/mysqld.sock"
else
null;
defaultText = literalExpression "null";
example = "/run/mysqld/mysqld.sock";
description = "Path to the unix socket file to use for authentication.";
};
path = mkOption {
type = types.str;
default = "${cfg.stateDir}/data/forgejo.db";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/forgejo.db"'';
description = "Path to the sqlite3 database file.";
};
createDatabase = mkOption {
type = types.bool;
default = true;
description = "Whether to create a local database automatically.";
};
};
dump = {
enable = mkEnableOption "periodic dumps via the [built-in {command}`dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump)";
interval = mkOption {
type = types.str;
default = "04:31";
example = "hourly";
description = ''
Run a Forgejo dump at this interval. Runs by default at 04:31 every day.
The format is described in
{manpage}`systemd.time(7)`.
'';
};
backupDir = mkOption {
type = types.str;
default = "${cfg.stateDir}/dump";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
description = "Path to the directory where the dump archives will be stored.";
};
type = mkOption {
type = types.enum [
"zip"
"tar"
"tar.sz"
"tar.gz"
"tar.xz"
"tar.bz2"
"tar.br"
"tar.lz4"
"tar.zst"
];
default = "zip";
description = "Archive format used to store the dump file.";
};
file = mkOption {
type = types.nullOr types.str;
default = null;
description = "Filename to be used for the dump. If `null` a default name is chosen by forgejo.";
example = "forgejo-dump";
};
};
lfs = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enables git-lfs support.";
};
contentDir = mkOption {
type = types.str;
default = "${cfg.stateDir}/data/lfs";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
description = "Where to store LFS files.";
};
};
repositoryRoot = mkOption {
type = types.str;
default = "${cfg.stateDir}/repositories";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
description = "Path to the git repositories.";
};
settings = mkOption {
default = { };
description = ''
Free-form settings written directly to the `app.ini` configfile file.
Refer to <https://forgejo.org/docs/latest/admin/config-cheat-sheet/> for supported values.
'';
example = literalExpression ''
{
DEFAULT = {
RUN_MODE = "dev";
};
"cron.sync_external_users" = {
RUN_AT_START = true;
SCHEDULE = "@every 24h";
UPDATE_EXISTING = true;
};
mailer = {
ENABLED = true;
MAILER_TYPE = "sendmail";
FROM = "do-not-reply@example.org";
SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail";
};
other = {
SHOW_FOOTER_VERSION = false;
};
}
'';
type = types.submodule {
freeformType = format.type;
options = {
log = {
ROOT_PATH = mkOption {
default = "${cfg.stateDir}/log";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
type = types.str;
description = "Root path for log files.";
};
LEVEL = mkOption {
default = "Info";
type = types.enum [
"Trace"
"Debug"
"Info"
"Warn"
"Error"
"Critical"
];
description = "General log level.";
};
};
server = {
PROTOCOL = mkOption {
type = types.enum [
"http"
"https"
"fcgi"
"http+unix"
"fcgi+unix"
];
default = "http";
description = ''Listen protocol. `+unix` means "over unix", not "in addition to."'';
};
HTTP_ADDR = mkOption {
type = types.either types.str types.path;
default =
if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then
"/run/forgejo/forgejo.sock"
else
"0.0.0.0";
defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"'';
description = "Listen address. Must be a path when using a unix socket.";
};
HTTP_PORT = mkOption {
type = types.port;
default = 3000;
description = "Listen port. Ignored when using a unix socket.";
};
DOMAIN = mkOption {
type = types.str;
default = "localhost";
description = "Domain name of your server.";
};
ROOT_URL = mkOption {
type = types.str;
default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/";
defaultText = literalExpression ''"http://''${config.services.forgejo.settings.server.DOMAIN}:''${toString config.services.forgejo.settings.server.HTTP_PORT}/"'';
description = "Full public URL of Forgejo server.";
};
STATIC_ROOT_PATH = mkOption {
type = types.either types.str types.path;
default = cfg.package.data;
defaultText = literalExpression "config.${opt.package}.data";
example = "/var/lib/forgejo/data";
description = "Upper level of template and static files path.";
};
DISABLE_SSH = mkOption {
type = types.bool;
default = false;
description = "Disable external SSH feature.";
};
SSH_PORT = mkOption {
type = types.port;
default = 22;
example = 2222;
description = ''
SSH port displayed in clone URL.
The option is required to configure a service when the external visible port
differs from the local listening port i.e. if port forwarding is used.
'';
};
};
session = {
COOKIE_SECURE = mkOption {
type = types.bool;
default = false;
description = ''
Marks session cookies as "secure" as a hint for browsers to only send
them via HTTPS. This option is recommend, if Forgejo is being served over HTTPS.
'';
};
};
};
};
};
secrets = mkOption {
default = { };
description = ''
This is a small wrapper over systemd's `LoadCredential`.
It takes the same sections and keys as {option}`services.forgejo.settings`,
but the value of each key is a path instead of a string or bool.
The path is then loaded as credential, exported as environment variable
and then feed through
<https://codeberg.org/forgejo/forgejo/src/branch/forgejo/contrib/environment-to-ini/environment-to-ini.go>.
It does the required environment variable escaping for you.
::: {.note}
Keys specified here take priority over the ones in {option}`services.forgejo.settings`!
:::
'';
example = literalExpression ''
{
metrics = {
TOKEN = "/run/keys/forgejo-metrics-token";
};
camo = {
HMAC_KEY = "/run/keys/forgejo-camo-hmac";
};
service = {
HCAPTCHA_SECRET = "/run/keys/forgejo-hcaptcha-secret";
HCAPTCHA_SITEKEY = "/run/keys/forgejo-hcaptcha-sitekey";
};
}
'';
type = types.submodule {
freeformType = with types; attrsOf (attrsOf path);
options = { };
};
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user;
message = "services.forgejo.database.user must match services.forgejo.user if the database is to be automatically provisioned";
}
{
assertion = cfg.database.createDatabase && usePostgresql -> cfg.database.user == cfg.database.name;
message = ''
When creating a database via NixOS, the db user and db name must be equal!
If you already have an existing DB+user and this assertion is new, you can safely set
`services.forgejo.createDatabase` to `false` because removal of `ensureUsers`
and `ensureDatabases` doesn't have any effect.
'';
}
];
services.forgejo.settings = {
DEFAULT = {
RUN_MODE = mkDefault "prod";
RUN_USER = mkDefault cfg.user;
WORK_PATH = mkDefault cfg.stateDir;
};
database = mkMerge [
{
DB_TYPE = cfg.database.type;
}
(mkIf (useMysql || usePostgresql) {
HOST =
if cfg.database.socket != null then
cfg.database.socket
else
cfg.database.host + ":" + toString cfg.database.port;
NAME = cfg.database.name;
USER = cfg.database.user;
})
(mkIf useSqlite {
PATH = cfg.database.path;
})
(mkIf usePostgresql {
SSL_MODE = "disable";
})
];
repository = {
ROOT = cfg.repositoryRoot;
};
server = mkIf cfg.lfs.enable {
LFS_START_SERVER = true;
};
session = {
COOKIE_NAME = mkDefault "session";
};
security = {
INSTALL_LOCK = true;
};
lfs = mkIf cfg.lfs.enable {
PATH = cfg.lfs.contentDir;
};
};
services.forgejo.secrets = {
security = {
SECRET_KEY = "${cfg.customDir}/conf/secret_key";
INTERNAL_TOKEN = "${cfg.customDir}/conf/internal_token";
};
oauth2 = {
JWT_SECRET = "${cfg.customDir}/conf/oauth2_jwt_secret";
};
database = mkIf (cfg.database.passwordFile != null) {
PASSWD = cfg.database.passwordFile;
};
server = mkIf cfg.lfs.enable {
LFS_JWT_SECRET = "${cfg.customDir}/conf/lfs_jwt_secret";
};
};
services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
enable = mkDefault true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensureDBOwnership = true;
}
];
};
services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) {
enable = mkDefault true;
package = mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "ALL PRIVILEGES";
};
}
];
};
systemd.tmpfiles.rules = [
"d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
"z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
# If we have a folder or symlink with Forgejo locales, remove it
# And symlink the current Forgejo locales in place
"L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
]
++ optionals cfg.lfs.enable [
"d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
"z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
];
systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) {
description = "Forgejo secret bootstrap helper";
script = ''
if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then
${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}'
fi
if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then
${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}'
fi
${optionalString cfg.lfs.enable ''
if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then
${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}'
fi
''}
if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then
${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}'
fi
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = cfg.user;
Group = cfg.group;
ReadWritePaths = [ cfg.customDir ];
UMask = "0077";
};
};
systemd.services.forgejo = {
description = "Forgejo (Beyond coding. We forge.)";
after = [
"network.target"
]
++ optionals usePostgresql [
"postgresql.target"
]
++ optionals useMysql [
"mysql.service"
]
++ optionals (!cfg.useWizard) [
"forgejo-secrets.service"
];
requires =
optionals (cfg.database.createDatabase && usePostgresql) [
"postgresql.target"
]
++ optionals (cfg.database.createDatabase && useMysql) [
"mysql.service"
]
++ optionals (!cfg.useWizard) [
"forgejo-secrets.service"
];
wantedBy = [ "multi-user.target" ];
path = [
cfg.package
pkgs.git
pkgs.gnupg
];
# In older versions the secret naming for JWT was kind of confusing.
# The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
# wasn't persistent at all.
# To fix that, there is now the file oauth2_jwt_secret containing the
# values for JWT_SECRET and the file jwt_secret gets renamed to
# lfs_jwt_secret.
# We have to consider this to stay compatible with older installations.
preStart = ''
${optionalString (!cfg.useWizard) ''
function forgejo_setup {
config='${cfg.customDir}/conf/app.ini'
cp -f '${format.generate "app.ini" cfg.settings}' "$config"
chmod u+w "$config"
${lib.getExe' cfg.package "environment-to-ini"} --config "$config"
chmod u-w "$config"
}
(umask 027; forgejo_setup)
''}
# run migrations/init the database
${exe} migrate
# update all hooks' binary paths
${exe} admin regenerate hooks
# update command option in authorized_keys
if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
then
${exe} admin regenerate keys
fi
'';
serviceConfig = {
Type = "notify";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.stateDir;
ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid";
Restart = "always";
# Runtime directory and mode
RuntimeDirectory = "forgejo";
RuntimeDirectoryMode = "0755";
# Proc filesystem
ProcSubset = "pid";
ProtectProc = "invisible";
# Access write directories
ReadWritePaths = [
cfg.customDir
cfg.dump.backupDir
cfg.repositoryRoot
cfg.stateDir
cfg.lfs.contentDir
];
UMask = "0027";
# Capabilities
CapabilityBoundingSet = "";
# Security
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
# System Call Filtering
SystemCallArchitectures = "native";
SystemCallFilter = [
"~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"
"setrlimit"
];
# cfg.secrets
LoadCredential = map (e: "${e.env}:${e.path}") secrets;
};
environment = {
USER = cfg.user;
HOME = cfg.stateDir;
FORGEJO_WORK_DIR = cfg.stateDir;
FORGEJO_CUSTOM = cfg.customDir;
}
// lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets);
};
services.openssh.settings.AcceptEnv = mkIf (
!cfg.settings.server.START_SSH_SERVER or false
) "GIT_PROTOCOL";
users.users = mkIf (cfg.user == "forgejo") {
forgejo = {
home = cfg.stateDir;
useDefaultShell = true;
group = cfg.group;
isSystemUser = true;
};
};
users.groups = mkIf (cfg.group == "forgejo") {
forgejo = { };
};
systemd.services.forgejo-dump = mkIf cfg.dump.enable {
description = "forgejo dump";
after = [ "forgejo.service" ];
path = [ cfg.package ];
environment = {
USER = cfg.user;
HOME = cfg.stateDir;
FORGEJO_WORK_DIR = cfg.stateDir;
FORGEJO_CUSTOM = cfg.customDir;
};
serviceConfig = {
Type = "oneshot";
User = cfg.user;
ExecStart =
"${exe} dump --type ${cfg.dump.type}"
+ optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
WorkingDirectory = cfg.dump.backupDir;
};
};
systemd.timers.forgejo-dump = mkIf cfg.dump.enable {
description = "Forgejo dump timer";
partOf = [ "forgejo-dump.service" ];
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = cfg.dump.interval;
};
};
meta.doc = ./forgejo.md;
meta.maintainers = with lib.maintainers; [
bendlas
emilylange
pyrox0
];
}

View File

@@ -0,0 +1,102 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.freeswitch;
pkg = cfg.package;
configDirectory = pkgs.runCommand "freeswitch-config-d" { } ''
mkdir -p $out
cp -rT ${cfg.configTemplate} $out
chmod -R +w $out
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (fileName: filePath: ''
mkdir -p $out/$(dirname ${fileName})
cp ${filePath} $out/${fileName}
'') cfg.configDir
)}
'';
configPath = if cfg.enableReload then "/etc/freeswitch" else configDirectory;
in
{
options = {
services.freeswitch = {
enable = lib.mkEnableOption "FreeSWITCH";
enableReload = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Issue the `reloadxml` command to FreeSWITCH when configuration directory changes (instead of restart).
See [FreeSWITCH documentation](https://freeswitch.org/confluence/display/FREESWITCH/Reloading) for more info.
The configuration directory is exposed at {file}`/etc/freeswitch`.
See also `systemd.services.*.restartIfChanged`.
'';
};
configTemplate = lib.mkOption {
type = lib.types.path;
default = "${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
defaultText = lib.literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/vanilla"'';
example = lib.literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/minimal"'';
description = ''
Configuration template to use.
See available templates in [FreeSWITCH repository](https://github.com/signalwire/freeswitch/tree/master/conf).
You can also set your own configuration directory.
'';
};
configDir = lib.mkOption {
type = with lib.types; attrsOf path;
default = { };
example = lib.literalExpression ''
{
"freeswitch.xml" = ./freeswitch.xml;
"dialplan/default.xml" = pkgs.writeText "dialplan-default.xml" '''
[xml lines]
''';
}
'';
description = ''
Override file in FreeSWITCH config template directory.
Each top-level attribute denotes a file path in the configuration directory, its value is the file path.
See [FreeSWITCH documentation](https://freeswitch.org/confluence/display/FREESWITCH/Default+Configuration) for more info.
Also check available templates in [FreeSWITCH repository](https://github.com/signalwire/freeswitch/tree/master/conf).
'';
};
package = lib.mkPackageOption pkgs "freeswitch" { };
};
};
config = lib.mkIf cfg.enable {
environment.etc.freeswitch = lib.mkIf cfg.enableReload {
source = configDirectory;
};
systemd.services.freeswitch-config-reload = lib.mkIf cfg.enableReload {
before = [ "freeswitch.service" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ configDirectory ];
serviceConfig = {
ExecStart = "/run/current-system/systemd/bin/systemctl try-reload-or-restart freeswitch.service";
RemainAfterExit = true;
Type = "oneshot";
};
};
systemd.services.freeswitch = {
description = "Free and open-source application server for real-time communication";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "freeswitch";
ExecStart = "${pkg}/bin/freeswitch -nf \\
-mod ${pkg}/lib/freeswitch/mod \\
-conf ${configPath} \\
-base /var/lib/freeswitch";
ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
Restart = "on-failure";
RestartSec = "5s";
CPUSchedulingPolicy = "fifo";
};
};
environment.systemPackages = [ pkg ];
};
}

View File

@@ -0,0 +1,56 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.fstrim;
in
{
options = {
services.fstrim = {
enable = (
lib.mkEnableOption "periodic SSD TRIM of mounted partitions in background"
// {
default = true;
}
);
interval = lib.mkOption {
type = lib.types.str;
default = "weekly";
description = ''
How often we run fstrim. For most desktop and server systems
a sufficient trimming frequency is once a week.
The format is described in
{manpage}`systemd.time(7)`.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.packages = [ pkgs.util-linux ];
systemd.timers.fstrim = {
timerConfig = {
OnCalendar = [
""
cfg.interval
];
};
wantedBy = [ "timers.target" ];
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,287 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.gammu-smsd;
configFile = pkgs.writeText "gammu-smsd.conf" ''
[gammu]
Device = ${cfg.device.path}
Connection = ${cfg.device.connection}
SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"}
LogFormat = ${cfg.log.format}
${lib.optionalString (cfg.device.pin != null) "PIN = ${cfg.device.pin}"}
${cfg.extraConfig.gammu}
[smsd]
LogFile = ${cfg.log.file}
Service = ${cfg.backend.service}
${lib.optionalString (cfg.backend.service == "files") ''
InboxPath = ${cfg.backend.files.inboxPath}
OutboxPath = ${cfg.backend.files.outboxPath}
SentSMSPath = ${cfg.backend.files.sentSMSPath}
ErrorSMSPath = ${cfg.backend.files.errorSMSPath}
''}
${lib.optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "sqlite") ''
Driver = ${cfg.backend.sql.driver}
DBDir = ${cfg.backend.sql.database}
''}
${lib.optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") (
with cfg.backend;
''
Driver = ${sql.driver}
${lib.optionalString (sql.database != null) "Database = ${sql.database}"}
${lib.optionalString (sql.host != null) "Host = ${sql.host}"}
${lib.optionalString (sql.user != null) "User = ${sql.user}"}
${lib.optionalString (sql.password != null) "Password = ${sql.password}"}
''
)}
${cfg.extraConfig.smsd}
'';
initDBDir = "share/doc/gammu/examples/sql";
gammuPackage =
with cfg.backend;
(pkgs.gammu.override {
dbiSupport = service == "sql" && sql.driver == "sqlite";
postgresSupport = service == "sql" && sql.driver == "native_pgsql";
});
in
{
options = {
services.gammu-smsd = {
enable = lib.mkEnableOption "gammu-smsd daemon";
user = lib.mkOption {
type = lib.types.str;
default = "smsd";
description = "User that has access to the device";
};
device = {
path = lib.mkOption {
type = lib.types.path;
description = "Device node or address of the phone";
example = "/dev/ttyUSB2";
};
group = lib.mkOption {
type = lib.types.str;
default = "root";
description = "Owner group of the device";
example = "dialout";
};
connection = lib.mkOption {
type = lib.types.str;
default = "at";
description = "Protocol which will be used to talk to the phone";
};
synchronizeTime = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to set time from computer to the phone during starting connection";
};
pin = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "PIN code for the simcard";
};
};
log = {
file = lib.mkOption {
type = lib.types.str;
default = "syslog";
description = "Path to file where information about communication will be stored";
};
format = lib.mkOption {
type = lib.types.enum [
"nothing"
"text"
"textall"
"textalldate"
"errors"
"errorsdate"
"binary"
];
default = "errors";
description = "Determines what will be logged to the LogFile";
};
};
extraConfig = {
gammu = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config lines to be added into [gammu] section";
};
smsd = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config lines to be added into [smsd] section";
};
};
backend = {
service = lib.mkOption {
type = lib.types.enum [
"null"
"files"
"sql"
];
default = "null";
description = "Service to use to store sms data.";
};
files = {
inboxPath = lib.mkOption {
type = lib.types.path;
default = "/var/spool/sms/inbox/";
description = "Where the received SMSes are stored";
};
outboxPath = lib.mkOption {
type = lib.types.path;
default = "/var/spool/sms/outbox/";
description = "Where SMSes to be sent should be placed";
};
sentSMSPath = lib.mkOption {
type = lib.types.path;
default = "/var/spool/sms/sent/";
description = "Where the transmitted SMSes are placed";
};
errorSMSPath = lib.mkOption {
type = lib.types.path;
default = "/var/spool/sms/error/";
description = "Where SMSes with error in transmission is placed";
};
};
sql = {
driver = lib.mkOption {
type = lib.types.enum [
"native_mysql"
"native_pgsql"
"odbc"
"dbi"
];
description = "DB driver to use";
};
sqlDialect = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "SQL dialect to use (odbc driver only)";
};
database = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Database name to store sms data";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Database server address";
};
user = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "User name used for connection to the database";
};
password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "User password used for connection to the database";
};
};
};
};
};
config = lib.mkIf cfg.enable {
users.users.${cfg.user} = {
description = "gammu-smsd user";
isSystemUser = true;
group = cfg.device.group;
};
environment.systemPackages =
with cfg.backend;
[ gammuPackage ] ++ lib.optionals (service == "sql" && sql.driver == "sqlite") [ pkgs.sqlite ];
systemd.services.gammu-smsd = {
description = "gammu-smsd daemon";
wantedBy = [ "multi-user.target" ];
wants =
with cfg.backend;
[ ] ++ lib.optionals (service == "sql" && sql.driver == "native_pgsql") [ "postgresql.target" ];
preStart =
with cfg.backend;
lib.optionalString (service == "files") (
with files;
''
mkdir -m 755 -p ${inboxPath} ${outboxPath} ${sentSMSPath} ${errorSMSPath}
chown ${cfg.user} -R ${inboxPath}
chown ${cfg.user} -R ${outboxPath}
chown ${cfg.user} -R ${sentSMSPath}
chown ${cfg.user} -R ${errorSMSPath}
''
)
+ lib.optionalString (service == "sql" && sql.driver == "sqlite") ''
cat "${gammuPackage}/${initDBDir}/sqlite.sql" \
| ${pkgs.sqlite.bin}/bin/sqlite3 ${sql.database}
''
+ (
let
execPsql =
extraArgs:
lib.concatStringsSep " " [
(lib.optionalString (sql.password != null) "PGPASSWORD=${sql.password}")
"${config.services.postgresql.package}/bin/psql"
(lib.optionalString (sql.host != null) "-h ${sql.host}")
(lib.optionalString (sql.user != null) "-U ${sql.user}")
"$extraArgs"
"${sql.database}"
];
in
lib.optionalString (service == "sql" && sql.driver == "native_pgsql") ''
echo '\i '"${gammuPackage}/${initDBDir}/pgsql.sql" | ${execPsql ""}
''
);
serviceConfig = {
User = "${cfg.user}";
Group = "${cfg.device.group}";
PermissionsStartOnly = true;
ExecStart = "${gammuPackage}/bin/gammu-smsd -c ${configFile}";
};
};
};
}

View File

@@ -0,0 +1,260 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.geoipupdate;
inherit (builtins)
isAttrs
isString
isInt
isList
typeOf
hashString
;
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"geoip-updater"
] "services.geoip-updater has been removed, use services.geoipupdate instead.")
];
options = {
services.geoipupdate = {
enable = lib.mkEnableOption ''
periodic downloading of GeoIP databases using geoipupdate
'';
interval = lib.mkOption {
type = lib.types.str;
default = "weekly";
description = ''
Update the GeoIP databases at this time / interval.
The format is described in
{manpage}`systemd.time(7)`.
'';
};
settings = lib.mkOption {
example = lib.literalExpression ''
{
AccountID = 200001;
DatabaseDirectory = "/var/lib/GeoIP";
LicenseKey = { _secret = "/run/keys/maxmind_license_key"; };
Proxy = "10.0.0.10:8888";
ProxyUserPassword = { _secret = "/run/keys/proxy_pass"; };
}
'';
description = ''
geoipupdate configuration options. See
<https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md>
for a full list of available options.
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}`GeoIP.conf` file, the
`ProxyUserPassword` key will be set to the
contents of the
{file}`/run/keys/proxy_pass` file.
'';
type = lib.types.submodule {
freeformType =
with lib.types;
let
type = oneOf [
str
int
bool
];
in
attrsOf (either type (listOf type));
options = {
AccountID = lib.mkOption {
type = lib.types.int;
description = ''
Your MaxMind account ID.
'';
};
EditionIDs = lib.mkOption {
type = with lib.types; listOf (either str int);
example = [
"GeoLite2-ASN"
"GeoLite2-City"
"GeoLite2-Country"
];
description = ''
List of database edition IDs. This includes new string
IDs like `GeoIP2-City` and old
numeric IDs like `106`.
'';
};
LicenseKey = lib.mkOption {
type = with lib.types; either path (attrsOf path);
description = ''
A file containing the MaxMind license key.
Always handled as a secret whether the value is
wrapped in a `{ _secret = ...; }`
attrset or not (refer to [](#opt-services.geoipupdate.settings) for
details).
'';
apply = x: if isAttrs x then x else { _secret = x; };
};
DatabaseDirectory = lib.mkOption {
type = lib.types.path;
default = "/var/lib/GeoIP";
example = "/run/GeoIP";
description = ''
The directory to store the database files in. The
directory will be automatically created, the owner
changed to `geoip` and permissions
set to world readable. This applies if the directory
already exists as well, so don't use a directory with
sensitive contents.
'';
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
services.geoipupdate.settings = {
LockFile = "/run/geoipupdate/.lock";
};
systemd.services.geoipupdate-create-db-dir = {
serviceConfig.Type = "oneshot";
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
mkdir -p ${cfg.settings.DatabaseDirectory}
chmod 0755 ${cfg.settings.DatabaseDirectory}
'';
};
systemd.services.geoipupdate = {
description = "GeoIP Updater";
requires = [ "geoipupdate-create-db-dir.service" ];
after = [
"geoipupdate-create-db-dir.service"
"network-online.target"
"nss-lookup.target"
];
path = [ pkgs.replace-secret ];
wants = [ "network-online.target" ];
startAt = cfg.interval;
serviceConfig = {
ExecStartPre =
let
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
geoipupdateKeyValue = lib.generators.toKeyValue {
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
mkValueString =
v:
if isInt v then
toString v
else if isString v then
v
else if true == v then
"1"
else if false == v then
"0"
else if isList v then
lib.concatMapStringsSep " " mkValueString v
else if isSecret v then
hashString "sha256" v._secret
else
throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}";
};
};
secretPaths = lib.catAttrs "_secret" (lib.collect isSecret cfg.settings);
mkSecretReplacement = file: ''
replace-secret ${
lib.escapeShellArgs [
(hashString "sha256" file)
file
"/run/geoipupdate/GeoIP.conf"
]
}
'';
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
chown geoip "${cfg.settings.DatabaseDirectory}"
cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
${secretReplacements}
'';
in
"+${pkgs.writeShellScript "start-pre-full-privileges" script}";
ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf";
User = "geoip";
DynamicUser = true;
ReadWritePaths = cfg.settings.DatabaseDirectory;
RuntimeDirectory = "geoipupdate";
RuntimeDirectoryMode = "0700";
CapabilityBoundingSet = "";
PrivateDevices = true;
PrivateMounts = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictRealtime = true;
RestrictNamespaces = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
SystemCallArchitectures = "native";
};
};
systemd.timers.geoipupdate-initial-run = {
wantedBy = [ "timers.target" ];
unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
timerConfig = {
Unit = "geoipupdate.service";
OnActiveSec = 0;
};
};
};
meta.maintainers = [ lib.maintainers.talyz ];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
# GitLab {#module-services-gitlab}
GitLab is a feature-rich git hosting service.
## Prerequisites {#module-services-gitlab-prerequisites}
The `gitlab` service exposes only an Unix socket at
`/run/gitlab/gitlab-workhorse.socket`. You need to
configure a webserver to proxy HTTP requests to the socket.
For instance, the following configuration could be used to use nginx as
frontend proxy:
```nix
{
services.nginx = {
enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
virtualHosts."git.example.com" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://unix:/run/gitlab/gitlab-workhorse.socket";
proxyWebsockets = true;
};
};
};
}
```
## Configuring {#module-services-gitlab-configuring}
GitLab depends on both PostgreSQL and Redis and will automatically enable
both services. In the case of PostgreSQL, a database and a role will be
created.
The default state dir is `/var/gitlab/state`. This is where
all data like the repositories and uploads will be stored.
A basic configuration with some custom settings could look like this:
```nix
{
services.gitlab = {
enable = true;
databasePasswordFile = "/var/keys/gitlab/db_password";
initialRootPasswordFile = "/var/keys/gitlab/root_password";
https = true;
host = "git.example.com";
port = 443;
user = "git";
group = "git";
smtp = {
enable = true;
address = "localhost";
port = 25;
};
secrets = {
dbFile = "/var/keys/gitlab/db";
secretFile = "/var/keys/gitlab/secret";
otpFile = "/var/keys/gitlab/otp";
jwsFile = "/var/keys/gitlab/jws";
};
extraConfig = {
gitlab = {
email_from = "gitlab-no-reply@example.com";
email_display_name = "Example GitLab";
email_reply_to = "gitlab-no-reply@example.com";
default_projects_features = {
builds = false;
};
};
};
};
}
```
If you're setting up a new GitLab instance, generate new
secrets. You for instance use
`tr -dc A-Za-z0-9 < /dev/urandom | head -c 128 > /var/keys/gitlab/db` to
generate a new db secret. Make sure the files can be read by, and
only by, the user specified by
[services.gitlab.user](#opt-services.gitlab.user). GitLab
encrypts sensitive data stored in the database. If you're restoring
an existing GitLab instance, you must specify the secrets secret
from `config/secrets.yml` located in your GitLab
state folder.
When `incoming_mail.enabled` is set to `true`
in [extraConfig](#opt-services.gitlab.extraConfig) an additional
service called `gitlab-mailroom` is enabled for fetching incoming mail.
Refer to [](#ch-options) for all available configuration
options for the [services.gitlab](#opt-services.gitlab.enable) module.
## Maintenance {#module-services-gitlab-maintenance}
### Backups {#module-services-gitlab-maintenance-backups}
Backups can be configured with the options in
[services.gitlab.backup](#opt-services.gitlab.backup.keepTime). Use
the [services.gitlab.backup.startAt](#opt-services.gitlab.backup.startAt)
option to configure regular backups.
To run a manual backup, start the `gitlab-backup` service:
```ShellSession
$ systemctl start gitlab-backup.service
```
### Rake tasks {#module-services-gitlab-maintenance-rake}
You can run GitLab's rake tasks with `gitlab-rake`
which will be available on the system when GitLab is enabled. You
will have to run the command as the user that you configured to run
GitLab with.
A list of all available rake tasks can be obtained by running:
```ShellSession
$ sudo -u git -H gitlab-rake -T
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.gitolite;
# Use writeTextDir to not leak Nix store hash into file name
pubkeyFile = (pkgs.writeTextDir "gitolite-admin.pub" cfg.adminPubkey) + "/gitolite-admin.pub";
hooks = lib.concatMapStrings (hook: "${hook} ") cfg.commonHooks;
in
{
options = {
services.gitolite = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable gitolite management under the
`gitolite` user. After
switching to a configuration with Gitolite enabled, you can
then run `git clone gitolite@host:gitolite-admin.git` to manage it further.
'';
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/gitolite";
description = ''
The gitolite home directory used to store all repositories. If left as the default value
this directory will automatically be created before the gitolite server starts, otherwise
the sysadmin is responsible for ensuring the directory exists with appropriate ownership
and permissions.
'';
};
adminPubkey = lib.mkOption {
type = lib.types.str;
description = ''
Initial administrative public key for Gitolite. This should
be an SSH Public Key. Note that this key will only be used
once, upon the first initialization of the Gitolite user.
The key string cannot have any line breaks in it.
'';
};
enableGitAnnex = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable git-annex support. Uses the `extraGitoliteRc` option
to apply the necessary configuration.
'';
};
commonHooks = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
A list of custom git hooks that get copied to `~/.gitolite/hooks/common`.
'';
};
extraGitoliteRc = lib.mkOption {
type = lib.types.lines;
default = "";
example = lib.literalExpression ''
'''
$RC{UMASK} = 0027;
$RC{SITE_INFO} = 'This is our private repository host';
push( @{$RC{ENABLE}}, 'Kindergarten' ); # enable the command/feature
@{$RC{ENABLE}} = grep { $_ ne 'desc' } @{$RC{ENABLE}}; # disable the command/feature
'''
'';
description = ''
Extra configuration to append to the default `~/.gitolite.rc`.
This should be Perl code that modifies the `%RC`
configuration variable. The default `~/.gitolite.rc`
content is generated by invoking `gitolite print-default-rc`,
and extra configuration from this option is appended to it. The result
is placed to Nix store, and the `~/.gitolite.rc` file
becomes a symlink to it.
If you already have a customized (or otherwise changed)
`~/.gitolite.rc` file, NixOS will refuse to replace
it with a symlink, and the `gitolite-init` initialization service
will fail. In this situation, in order to use this option, you
will need to take any customizations you may have in
`~/.gitolite.rc`, convert them to appropriate Perl
statements, add them to this option, and remove the file.
See also the `enableGitAnnex` option.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "gitolite";
description = ''
Gitolite user account. This is the username of the gitolite endpoint.
'';
};
description = lib.mkOption {
type = lib.types.str;
default = "Gitolite user";
description = ''
Gitolite user account's description.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "gitolite";
description = ''
Primary group of the Gitolite user account.
'';
};
};
};
config = lib.mkIf cfg.enable (
let
manageGitoliteRc = cfg.extraGitoliteRc != "";
rcDir = pkgs.runCommand "gitolite-rc" { preferLocalBuild = true; } rcDirScript;
rcDirScript = ''
mkdir "$out"
export HOME=temp-home
mkdir -p "$HOME/.gitolite/logs" # gitolite can't run without it
'${pkgs.gitolite}'/bin/gitolite print-default-rc >>"$out/gitolite.rc.default"
cat <<END >>"$out/gitolite.rc"
# This file is managed by NixOS.
# Use services.gitolite options to control it.
END
cat "$out/gitolite.rc.default" >>"$out/gitolite.rc"
''
+ lib.optionalString (cfg.extraGitoliteRc != "") ''
echo -n ${lib.escapeShellArg ''
# Added by NixOS:
${lib.removeSuffix "\n" cfg.extraGitoliteRc}
# per perl rules, this should be the last line in such a file:
1;
''} >>"$out/gitolite.rc"
'';
in
{
services.gitolite.extraGitoliteRc = lib.optionalString cfg.enableGitAnnex ''
# Enable git-annex support:
push( @{$RC{ENABLE}}, 'git-annex-shell ua');
'';
users.users.${cfg.user} = {
description = cfg.description;
home = cfg.dataDir;
uid = config.ids.uids.gitolite;
group = cfg.group;
useDefaultShell = true;
};
users.groups.${cfg.group}.gid = config.ids.gids.gitolite;
systemd.services.gitolite-init = {
description = "Gitolite initialization";
wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = cfg.dataDir;
environment = {
GITOLITE_RC = ".gitolite.rc";
GITOLITE_RC_DEFAULT = "${rcDir}/gitolite.rc.default";
};
serviceConfig = lib.mkMerge [
(lib.mkIf (cfg.dataDir == "/var/lib/gitolite") {
StateDirectory = "gitolite gitolite/.gitolite gitolite/.gitolite/logs";
StateDirectoryMode = "0750";
})
{
Type = "oneshot";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = "~";
RemainAfterExit = true;
}
];
path = [
pkgs.gitolite
pkgs.git
pkgs.perl
pkgs.bash
pkgs.diffutils
config.programs.ssh.package
];
script =
let
rcSetupScriptIfCustomFile =
if manageGitoliteRc then
''
cat <<END
<3>ERROR: NixOS can't apply declarative configuration
<3>to your .gitolite.rc file, because it seems to be
<3>already customized manually.
<3>See the services.gitolite.extraGitoliteRc option
<3>in "man configuration.nix" for more information.
END
# Not sure if the line below addresses the issue directly or just
# adds a delay, but without it our error message often doesn't
# show up in `systemctl status gitolite-init`.
journalctl --flush
exit 1
''
else
''
:
'';
rcSetupScriptIfDefaultFileOrStoreSymlink =
if manageGitoliteRc then
''
ln -sf "${rcDir}/gitolite.rc" "$GITOLITE_RC"
''
else
''
[[ -L "$GITOLITE_RC" ]] && rm -f "$GITOLITE_RC"
'';
in
''
if ( [[ ! -e "$GITOLITE_RC" ]] && [[ ! -L "$GITOLITE_RC" ]] ) ||
( [[ -f "$GITOLITE_RC" ]] && diff -q "$GITOLITE_RC" "$GITOLITE_RC_DEFAULT" >/dev/null ) ||
( [[ -L "$GITOLITE_RC" ]] && [[ "$(readlink "$GITOLITE_RC")" =~ ^/nix/store/ ]] )
then
''
+ rcSetupScriptIfDefaultFileOrStoreSymlink
+ ''
else
''
+ rcSetupScriptIfCustomFile
+ ''
fi
if [ ! -d repositories ]; then
gitolite setup -pk ${pubkeyFile}
fi
if [ -n "${hooks}" ]; then
cp -f ${hooks} .gitolite/hooks/common/
chmod +x .gitolite/hooks/common/*
fi
gitolite setup # Upgrade if needed
'';
};
environment.systemPackages = [
pkgs.gitolite
pkgs.git
]
++ lib.optional cfg.enableGitAnnex pkgs.git-annex;
}
);
}

View File

@@ -0,0 +1,62 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.gitweb;
in
{
options.services.gitweb = {
projectroot = lib.mkOption {
default = "/srv/git";
type = lib.types.path;
description = ''
Path to git projects (bare repositories) that should be served by
gitweb. Must not end with a slash.
'';
};
extraConfig = lib.mkOption {
default = "";
type = lib.types.lines;
description = ''
Verbatim configuration text appended to the generated gitweb.conf file.
'';
example = ''
$feature{'highlight'}{'default'} = [1];
$feature{'ctags'}{'default'} = [1];
$feature{'avatar'}{'default'} = ['gravatar'];
'';
};
gitwebTheme = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Use an alternative theme for gitweb, strongly inspired by GitHub.
'';
};
gitwebConfigFile = lib.mkOption {
default = pkgs.writeText "gitweb.conf" ''
# path to git projects (<project>.git)
$projectroot = "${cfg.projectroot}";
$highlight_bin = "${pkgs.highlight}/bin/highlight";
${cfg.extraConfig}
'';
defaultText = lib.literalMD "generated config file";
type = lib.types.path;
readOnly = true;
internal = true;
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,175 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.gollum;
in
{
imports = [
(lib.mkRemovedOptionModule
[
"services"
"gollum"
"mathjax"
]
"MathJax rendering might be discontinued in the future, use services.gollum.math instead to enable KaTeX rendering or file a PR if you really need Mathjax"
)
];
options.services.gollum = {
enable = lib.mkEnableOption "Gollum, a git-powered wiki service";
address = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "IP address on which the web server will listen.";
};
port = lib.mkOption {
type = lib.types.port;
default = 4567;
description = "Port on which the web server will run.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Content of the configuration file";
};
math = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable support for math rendering using KaTeX";
};
allowUploads = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"dir"
"page"
]
);
default = null;
description = "Enable uploads of external files";
};
user-icons = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"gravatar"
"identicon"
]
);
default = null;
description = "Enable specific user icons for history view";
};
emoji = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Parse and interpret emoji tags";
};
h1-title = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use the first h1 as page title";
};
no-edit = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable editing pages";
};
local-time = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use the browser's local timezone instead of the server's for displaying dates.";
};
branch = lib.mkOption {
type = lib.types.str;
default = "master";
example = "develop";
description = "Git branch to serve";
};
stateDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/gollum";
description = "Specifies the path of the repository directory. If it does not exist, Gollum will create it on startup.";
};
package = lib.mkPackageOption pkgs "gollum" { };
user = lib.mkOption {
type = lib.types.str;
default = "gollum";
description = "Specifies the owner of the wiki directory";
};
group = lib.mkOption {
type = lib.types.str;
default = "gollum";
description = "Specifies the owner group of the wiki directory";
};
};
config = lib.mkIf cfg.enable {
users.users.gollum = lib.mkIf (cfg.user == "gollum") {
group = cfg.group;
description = "Gollum user";
createHome = false;
isSystemUser = true;
};
users.groups."${cfg.group}" = { };
systemd.tmpfiles.rules = [ "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -" ];
systemd.services.gollum = {
description = "Gollum wiki";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.git ];
preStart = ''
# This is safe to be run on an existing repo
git init ${cfg.stateDir}
'';
serviceConfig = {
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.stateDir;
ExecStart = ''
${cfg.package}/bin/gollum \
--port ${toString cfg.port} \
--host ${cfg.address} \
--config ${pkgs.writeText "gollum-config.rb" cfg.extraConfig} \
--ref ${cfg.branch} \
${lib.optionalString cfg.math "--math"} \
${lib.optionalString cfg.emoji "--emoji"} \
${lib.optionalString cfg.h1-title "--h1-title"} \
${lib.optionalString cfg.no-edit "--no-edit"} \
${lib.optionalString cfg.local-time "--local-time"} \
${lib.optionalString (cfg.allowUploads != null) "--allow-uploads ${cfg.allowUploads}"} \
${lib.optionalString (cfg.user-icons != null) "--user-icons ${cfg.user-icons}"} \
${cfg.stateDir}
'';
};
};
};
meta.maintainers = with lib.maintainers; [
erictapen
bbenno
];
}

View File

@@ -0,0 +1,356 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.gotenberg;
args = [
"--api-port=${toString cfg.port}"
"--api-timeout=${cfg.timeout}"
"--api-root-path=${cfg.rootPath}"
"--log-level=${cfg.logLevel}"
"--chromium-max-queue-size=${toString cfg.chromium.maxQueueSize}"
"--libreoffice-restart-after=${toString cfg.libreoffice.restartAfter}"
"--libreoffice-max-queue-size=${toString cfg.libreoffice.maxQueueSize}"
"--pdfengines-merge-engines=${lib.concatStringsSep "," cfg.pdfEngines.merge}"
"--pdfengines-convert-engines=${lib.concatStringsSep "," cfg.pdfEngines.convert}"
"--pdfengines-read-metadata-engines=${lib.concatStringsSep "," cfg.pdfEngines.readMetadata}"
"--pdfengines-write-metadata-engines=${lib.concatStringsSep "," cfg.pdfEngines.writeMetadata}"
"--api-download-from-allow-list=${cfg.downloadFrom.allowList}"
"--api-download-from-max-retry=${toString cfg.downloadFrom.maxRetries}"
]
++ optional cfg.enableBasicAuth "--api-enable-basic-auth"
++ optional cfg.chromium.autoStart "--chromium-auto-start"
++ optional cfg.chromium.disableJavascript "--chromium-disable-javascript"
++ optional cfg.chromium.disableRoutes "--chromium-disable-routes"
++ optional cfg.libreoffice.autoStart "--libreoffice-auto-start"
++ optional cfg.libreoffice.disableRoutes "--libreoffice-disable-routes"
++ optional cfg.pdfEngines.disableRoutes "--pdfengines-disable-routes"
++ optional (
cfg.downloadFrom.denyList != null
) "--api-download-from-deny-list=${cfg.downloadFrom.denyList}"
++ optional cfg.downloadFrom.disable "--api-disable-download-from"
++ optional (cfg.bodyLimit != null) "--api-body-limit=${cfg.bodyLimit}"
++ lib.optionals (cfg.extraArgs != [ ]) cfg.extraArgs;
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
types
mkIf
optional
optionalAttrs
;
in
{
options = {
services.gotenberg = {
enable = mkEnableOption "Gotenberg, a stateless API for PDF files";
# Users can override only gotenberg, libreoffice and chromium if they want to (eg. ungoogled-chromium, different LO version, etc)
# Don't allow setting the qpdf, pdftk, or unoconv paths, as those are very stable
# and there's only one version of each.
package = mkPackageOption pkgs "gotenberg" { };
port = mkOption {
type = types.port;
default = 3000;
description = "Port on which the API should listen.";
};
bindIP = mkOption {
type = types.nullOr types.str;
default = "127.0.0.1";
description = "Port the API listener should bind to. Set to 0.0.0.0 to listen on all available IPs.";
};
timeout = mkOption {
type = types.nullOr types.str;
default = "30s";
description = "Timeout for API requests.";
};
rootPath = mkOption {
type = types.str;
default = "/";
description = "Root path for the Gotenberg API.";
};
enableBasicAuth = mkOption {
type = types.bool;
default = false;
description = ''
HTTP Basic Authentication.
If you set this, be sure to set `GOTENBERG_API_BASIC_AUTH_USERNAME`and `GOTENBERG_API_BASIC_AUTH_PASSWORD`
in your `services.gotenberg.environmentFile` file.
'';
};
bodyLimit = mkOption {
type = types.nullOr types.str;
default = null;
description = "Sets the max limit for `multipart/form-data` requests. Accepts values like '5M', '20G', etc.";
};
extraFontPackages = mkOption {
type = types.listOf types.package;
default = [ ];
description = "Extra fonts to make available.";
};
chromium = {
package = mkPackageOption pkgs "chromium" { };
maxQueueSize = mkOption {
type = types.ints.unsigned;
default = 0;
description = "Maximum queue size for chromium-based conversions. Setting to 0 disables the limit.";
};
autoStart = mkOption {
type = types.bool;
default = false;
description = "Automatically start Chromium when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it.";
};
disableJavascript = mkOption {
type = types.bool;
default = false;
description = "Disable Javascript execution.";
};
disableRoutes = mkOption {
type = types.bool;
default = false;
description = "Disable all routes allowing Chromium-based conversion.";
};
};
downloadFrom = {
allowList = mkOption {
type = types.nullOr types.str;
default = ".*";
description = "Allow these URLs to be used in the `downloadFrom` API field. Accepts a regular expression.";
};
denyList = mkOption {
type = types.nullOr types.str;
default = null;
description = "Deny accepting URLs from these domains in the `downloadFrom` API field. Accepts a regular expression.";
};
maxRetries = mkOption {
type = types.ints.unsigned;
default = 4;
description = "The maximum amount of times to retry downloading a file specified with `downloadFrom`.";
};
disable = mkOption {
type = types.bool;
default = false;
description = "Whether to disable the ability to download files for conversion from outside sources.";
};
};
libreoffice = {
package = mkPackageOption pkgs "libreoffice" { };
restartAfter = mkOption {
type = types.ints.unsigned;
default = 10;
description = "Restart LibreOffice after this many conversions. Setting to 0 disables this feature.";
};
maxQueueSize = mkOption {
type = types.ints.unsigned;
default = 0;
description = "Maximum queue size for LibreOffice-based conversions. Setting to 0 disables the limit.";
};
autoStart = mkOption {
type = types.bool;
default = false;
description = "Automatically start LibreOffice when Gotenberg starts. If false, LibreOffice will start on the first conversion request that uses it.";
};
disableRoutes = mkOption {
type = types.bool;
default = false;
description = "Disable all routes allowing LibreOffice-based conversion.";
};
};
pdfEngines = {
merge = mkOption {
type = types.listOf (
types.enum [
"qpdf"
"pdfcpu"
"pdftk"
]
);
default = [
"qpdf"
"pdfcpu"
"pdftk"
];
description = "PDF Engines to use for merging files.";
};
convert = mkOption {
type = types.listOf (
types.enum [
"libreoffice-pdfengine"
]
);
default = [
"libreoffice-pdfengine"
];
description = "PDF Engines to use for converting files.";
};
readMetadata = mkOption {
type = types.listOf (
types.enum [
"exiftool"
]
);
default = [
"exiftool"
];
description = "PDF Engines to use for reading metadata from files.";
};
writeMetadata = mkOption {
type = types.listOf (
types.enum [
"exiftool"
]
);
default = [
"exiftool"
];
description = "PDF Engines to use for writing metadata to files.";
};
disableRoutes = mkOption {
type = types.bool;
default = false;
description = "Disable routes related to PDF engines.";
};
};
logLevel = mkOption {
type = types.enum [
"error"
"warn"
"info"
"debug"
];
default = "info";
description = "The logging level for Gotenberg.";
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Environment file to load extra environment variables from.";
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Any extra command-line flags to pass to the Gotenberg service.";
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.enableBasicAuth -> cfg.environmentFile != null;
message = ''
When enabling HTTP Basic Authentication with `services.gotenberg.enableBasicAuth`,
you must provide an environment file via `services.gotenberg.environmentFile` with the appropriate environment variables set in it.
See `services.gotenberg.enableBasicAuth` for the names of those variables.
'';
}
{
assertion = !(lib.isList cfg.pdfEngines);
message = ''
Setting `services.gotenberg.pdfEngines` to a list is now deprecated.
Use the new `pdfEngines.mergeEngines`, `pdfEngines.convertEngines`, `pdfEngines.readMetadataEngines`, and `pdfEngines.writeMetadataEngines` settings instead.
The previous option was using a method that is now deprecated by upstream.
'';
}
];
systemd.services.gotenberg = {
description = "Gotenberg API server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ cfg.package ];
environment = {
LIBREOFFICE_BIN_PATH = "${cfg.libreoffice.package}/lib/libreoffice/program/soffice.bin";
CHROMIUM_BIN_PATH = lib.getExe cfg.chromium.package;
FONTCONFIG_FILE = pkgs.makeFontsConf {
fontDirectories = [ pkgs.liberation_ttf_v2 ] ++ cfg.extraFontPackages;
};
# Needed for LibreOffice to work correctly.
# https://github.com/NixOS/nixpkgs/issues/349123#issuecomment-2418330936
HOME = "/run/gotenberg";
};
serviceConfig = {
Type = "simple";
# NOTE: disable to debug chromium crashes or otherwise no coredump is created and forbidden syscalls are not being logged
DynamicUser = true;
ExecStart = "${lib.getExe cfg.package} ${lib.escapeShellArgs args}";
# Needed for LibreOffice to work correctly.
# See above issue comment.
WorkingDirectory = "/run/gotenberg";
RuntimeDirectory = "gotenberg";
# Hardening options
PrivateDevices = true;
PrivateIPC = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
LockPersonality = true;
SystemCallFilter = [
"@sandbox"
"@system-service"
"@chown"
"@pkey" # required by chromium or it crashes
"mincore"
];
SystemCallArchitectures = "native";
UMask = 77;
}
// optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; };
};
};
meta.maintainers = with lib.maintainers; [ pyrox0 ];
}

View File

@@ -0,0 +1,154 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
uid = config.ids.uids.gpsd;
gid = config.ids.gids.gpsd;
cfg = config.services.gpsd;
in
{
###### interface
imports = [
(lib.mkRemovedOptionModule [ "services" "gpsd" "device" ] "Use `services.gpsd.devices` instead.")
];
options = {
services.gpsd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable `gpsd`, a GPS service daemon.
'';
};
devices = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "/dev/ttyUSB0" ];
description = ''
List of devices that `gpsd` should subscribe to.
A device may be a local serial device for GPS input, or a
URL of the form:
`[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]` in
which case it specifies an input source for DGPS or ntrip
data.
'';
};
readonly = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable the broken-device-safety, otherwise
known as read-only mode. Some popular bluetooth and USB
receivers lock up or become totally inaccessible when
probed or reconfigured. This switch prevents gpsd from
writing to a receiver. This means that gpsd cannot
configure the receiver for optimal performance, but it
also means that gpsd cannot break the receiver. A better
solution would be for Bluetooth to not be so fragile. A
platform independent method to identify
serial-over-Bluetooth devices would also be nice.
'';
};
nowait = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
don't wait for client connects to poll GPS
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 2947;
description = ''
The port where to listen for TCP connections.
'';
};
debugLevel = lib.mkOption {
type = lib.types.int;
default = 0;
description = ''
The debugging level.
'';
};
listenany = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Listen on all addresses rather than just loopback.
'';
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"-r"
"-s"
"19200"
];
description = ''
A list of extra command line arguments to pass to gpsd.
Check {manpage}`gpsd(8)` mangpage for possible arguments.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.users.gpsd = {
inherit uid;
group = "gpsd";
description = "gpsd daemon user";
home = "/var/empty";
};
users.groups.gpsd = { inherit gid; };
systemd.services.gpsd = {
description = "GPSD daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart =
let
devices = utils.escapeSystemdExecArgs cfg.devices;
extraArgs = utils.escapeSystemdExecArgs cfg.extraArgs;
in
''
${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}" \
-S "${toString cfg.port}" \
${lib.optionalString cfg.readonly "-b"} \
${lib.optionalString cfg.nowait "-n"} \
${lib.optionalString cfg.listenany "-G"} \
${extraArgs} \
${devices}
'';
};
};
};
}

View File

@@ -0,0 +1,65 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.graphical-desktop;
xcfg = config.services.xserver;
dmcfg = config.services.displayManager;
in
{
options = {
services.graphical-desktop.enable =
lib.mkEnableOption "bits and pieces required for a graphical desktop session"
// {
default = xcfg.enable || dmcfg.enable;
defaultText = lib.literalExpression "(config.services.xserver.enable || config.services.displayManager.enable)";
internal = true;
};
};
config = lib.mkIf cfg.enable {
environment = {
# localectl looks into 00-keyboard.conf
etc."X11/xorg.conf.d/00-keyboard.conf".text = ''
Section "InputClass"
Identifier "Keyboard catchall"
MatchIsKeyboard "on"
Option "XkbModel" "${xcfg.xkb.model}"
Option "XkbLayout" "${xcfg.xkb.layout}"
Option "XkbOptions" "${xcfg.xkb.options}"
Option "XkbVariant" "${xcfg.xkb.variant}"
EndSection
'';
systemPackages = with pkgs; [
nixos-icons # needed for gnome and pantheon about dialog, nixos-manual and maybe more
xdg-utils
];
};
fonts.enableDefaultPackages = lib.mkDefault true;
hardware.graphics.enable = lib.mkDefault true;
programs.gnupg.agent.pinentryPackage = lib.mkOverride 1100 pkgs.pinentry-gnome3;
services.speechd.enable = lib.mkDefault true;
services.pipewire = {
enable = lib.mkDefault true;
pulse.enable = lib.mkDefault true;
alsa.enable = lib.mkDefault true;
};
systemd.defaultUnit = lib.mkIf (xcfg.autorun || dmcfg.enable) "graphical.target";
xdg = {
autostart.enable = true;
menus.enable = true;
mime.enable = true;
icons.enable = true;
};
};
}

View File

@@ -0,0 +1,32 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.greenclip;
in
{
options.services.greenclip = {
enable = lib.mkEnableOption "Greenclip, a clipboard manager";
package = lib.mkPackageOption pkgs [ "haskellPackages" "greenclip" ] { };
};
config = lib.mkIf cfg.enable {
systemd.user.services.greenclip = {
enable = true;
description = "greenclip daemon";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/greenclip daemon";
Restart = "always";
};
};
environment.systemPackages = [ cfg.package ];
};
}

View File

@@ -0,0 +1,489 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.guix;
package = cfg.package.override { inherit (cfg) stateDir storeDir; };
guixBuildUser = id: {
name = "guixbuilder${toString id}";
group = cfg.group;
extraGroups = [ cfg.group ];
createHome = false;
description = "Guix build user ${toString id}";
isSystemUser = true;
};
guixBuildUsers =
numberOfUsers:
builtins.listToAttrs (
map (user: {
name = user.name;
value = user;
}) (builtins.genList guixBuildUser numberOfUsers)
);
# A set of Guix user profiles to be linked at activation. All of these should
# be default profiles managed by Guix CLI and the profiles are located in
# `${cfg.stateDir}/profiles/per-user/$USER/$PROFILE`.
guixUserProfiles = {
# The default Guix profile managed by `guix pull`. Take note this should be
# the profile with the most precedence in `PATH` env to let users use their
# updated versions of `guix` CLI.
"current-guix" = "\${XDG_CONFIG_HOME}/guix/current";
# The default Guix home profile. This profile contains more than exports
# such as an activation script at `$GUIX_HOME_PROFILE/activate`.
"guix-home" = "$HOME/.guix-home/profile";
# The default Guix profile similar to $HOME/.nix-profile from Nix.
"guix-profile" = "$HOME/.guix-profile";
};
# All of the Guix profiles to be used.
guixProfiles = lib.attrValues guixUserProfiles;
serviceEnv = {
GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale";
LC_ALL = "C.UTF-8";
};
# Currently, this is just done the lazy way with the official Guix script. A
# more "formal" way would be creating our own Guix script to handle and
# generate the ACL file ourselves.
aclFile = pkgs.runCommandLocal "guix-acl" { } ''
export GUIX_CONFIGURATION_DIRECTORY=./
for official_server_keys in ${lib.concatStringsSep " " cfg.substituters.authorizedKeys}; do
${lib.getExe' cfg.package "guix"} archive --authorize < "$official_server_keys"
done
install -Dm0600 ./acl "$out"
'';
in
{
meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
options.services.guix = with lib; {
enable = mkEnableOption "Guix build daemon service";
group = mkOption {
type = types.str;
default = "guixbuild";
example = "guixbuild";
description = ''
The group of the Guix build user pool.
'';
};
nrBuildUsers = mkOption {
type = types.ints.unsigned;
description = ''
Number of Guix build users to be used in the build pool.
'';
default = 10;
example = 20;
};
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = [
"--max-jobs=4"
"--debug"
];
description = ''
Extra flags to pass to the Guix daemon service.
'';
};
package = mkPackageOption pkgs "guix" {
extraDescription = ''
It should contain {command}`guix-daemon` and {command}`guix`
executable.
'';
};
storeDir = mkOption {
type = types.path;
default = "/gnu/store";
description = ''
The store directory where the Guix service will serve to/from. Take
note Guix cannot take advantage of substitutes if you set it something
other than {file}`/gnu/store` since most of the cached builds are
assumed to be in there.
::: {.warning}
This will also recompile all packages because the normal cache no
longer applies.
:::
'';
};
stateDir = mkOption {
type = types.path;
default = "/var";
description = ''
The state directory where Guix service will store its data such as its
user-specific profiles, cache, and state files.
::: {.warning}
Changing it to something other than the default will rebuild the
package.
:::
'';
example = "/gnu/var";
};
substituters = {
urls = lib.mkOption {
type = with lib.types; listOf str;
default = [
"https://ci.guix.gnu.org"
"https://bordeaux.guix.gnu.org"
"https://berlin.guix.gnu.org"
];
example = lib.literalExpression ''
options.services.guix.substituters.urls.default ++ [
"https://guix.example.com"
"https://guix.example.org"
]
'';
description = ''
A list of substitute servers' URLs for the Guix daemon to download
substitutes from.
'';
};
authorizedKeys = lib.mkOption {
type = with lib.types; listOf path;
default = [
"${cfg.package}/share/guix/ci.guix.gnu.org.pub"
"${cfg.package}/share/guix/bordeaux.guix.gnu.org.pub"
"${cfg.package}/share/guix/berlin.guix.gnu.org.pub"
];
defaultText = ''
The packaged signing keys from {option}`services.guix.package`.
'';
example = lib.literalExpression ''
options.services.guix.substituters.authorizedKeys.default ++ [
(builtins.fetchurl {
url = "https://guix.example.com/signing-key.pub";
})
(builtins.fetchurl {
url = "https://guix.example.org/static/signing-key.pub";
})
]
'';
description = ''
A list of signing keys for each substitute server to be authorized as
a source of substitutes. Without this, the listed substitute servers
from {option}`services.guix.substituters.urls` would be ignored [with
some
exceptions](https://guix.gnu.org/manual/en/html_node/Substitute-Authentication.html).
'';
};
};
publish = {
enable = mkEnableOption "substitute server for your Guix store directory";
generateKeyPair = mkOption {
type = types.bool;
description = ''
Whether to generate signing keys in {file}`/etc/guix` which are
required to initialize a substitute server. Otherwise,
`--public-key=$FILE` and `--private-key=$FILE` can be passed in
{option}`services.guix.publish.extraArgs`.
'';
default = true;
example = false;
};
port = mkOption {
type = types.port;
default = 8181;
example = 8200;
description = ''
Port of the substitute server to listen on.
'';
};
user = mkOption {
type = types.str;
default = "guix-publish";
description = ''
Name of the user to change once the server is up.
'';
};
extraArgs = mkOption {
type = with types; listOf str;
description = ''
Extra flags to pass to the substitute server.
'';
default = [ ];
example = [
"--compression=zstd:6"
"--discover=no"
];
};
};
gc = {
enable = mkEnableOption "automatic garbage collection service for Guix";
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
List of arguments to be passed to {command}`guix gc`.
When given no option, it will try to collect all garbage which is
often inconvenient so it is recommended to set [some
options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html).
'';
example = [
"--delete-generations=1m"
"--free-space=10G"
"--optimize"
];
};
dates = lib.mkOption {
type = types.str;
default = "03:15";
example = "weekly";
description = ''
How often the garbage collection occurs. This takes the time format
from {manpage}`systemd.time(7)`.
'';
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
environment.systemPackages = [ package ];
users.users = guixBuildUsers cfg.nrBuildUsers;
users.groups.${cfg.group} = { };
# Guix uses Avahi (through guile-avahi) both for the auto-discovering and
# advertising substitute servers in the local network.
services.avahi.enable = lib.mkDefault true;
services.avahi.publish.enable = lib.mkDefault true;
services.avahi.publish.userServices = lib.mkDefault true;
# It's similar to Nix daemon so there's no question whether or not this
# should be sandboxed.
systemd.services.guix-daemon = {
environment = serviceEnv // config.networking.proxy.envVars;
script = ''
exec ${lib.getExe' package "guix-daemon"} \
--build-users-group=${cfg.group} \
${
lib.optionalString (
cfg.substituters.urls != [ ]
) "--substitute-urls='${lib.concatStringsSep " " cfg.substituters.urls}'"
} \
${lib.escapeShellArgs cfg.extraArgs}
'';
serviceConfig = {
OOMPolicy = "continue";
RemainAfterExit = "yes";
Restart = "always";
TasksMax = 8192;
};
unitConfig.RequiresMountsFor = [
cfg.storeDir
cfg.stateDir
];
wantedBy = [ "multi-user.target" ];
};
# This is based from Nix daemon socket unit from upstream Nix package.
# Guix build daemon has support for systemd-style socket activation.
systemd.sockets.guix-daemon = {
description = "Guix daemon socket";
before = [ "multi-user.target" ];
listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ];
unitConfig.RequiresMountsFor = [
cfg.storeDir
cfg.stateDir
];
wantedBy = [ "sockets.target" ];
};
systemd.mounts = [
{
description = "Guix read-only store directory";
before = [ "guix-daemon.service" ];
what = cfg.storeDir;
where = cfg.storeDir;
type = "none";
options = "bind,ro";
unitConfig.DefaultDependencies = false;
wantedBy = [ "guix-daemon.service" ];
}
];
# Make transferring files from one store to another easier with the usual
# case being of most substitutes from the official Guix CI instance.
environment.etc."guix/acl".source = aclFile;
# Link the usual Guix profiles to the home directory. This is useful in
# ephemeral setups where only certain part of the filesystem is
# persistent (e.g., "Erase my darlings"-type of setup).
system.userActivationScripts.guix-activate-user-profiles.text =
let
guixProfile = profile: "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}";
linkProfile =
profile: location:
let
userProfile = guixProfile profile;
in
''
[ -d "${userProfile}" ] && ln -sfn "${userProfile}" "${location}"
'';
linkProfileToPath =
acc: profile: location:
acc + (linkProfile profile location);
# This should contain export-only Guix user profiles. The rest of it is
# handled manually in the activation script.
guixUserProfiles' = lib.attrsets.removeAttrs guixUserProfiles [ "guix-home" ];
linkExportsScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles';
in
''
# Don't export this please! It is only expected to be used for this
# activation script and nothing else.
XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
# Linking the usual Guix profiles into the home directory.
${linkExportsScript}
# Activate all of the default Guix non-exports profiles manually.
${linkProfile "guix-home" "$HOME/.guix-home"}
[ -L "$HOME/.guix-home" ] && "$HOME/.guix-home/activate"
'';
# GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by
# virtually every Guix-built packages. This is so that Guix-installed
# applications wouldn't use incompatible locale data and not touch its host
# system.
environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles;
# What Guix profiles export is very similar to Nix profiles so it is
# acceptable to list it here. Also, it is more likely that the user would
# want to use packages explicitly installed from Guix so we're putting it
# first.
environment.profiles = lib.mkBefore guixProfiles;
}
(lib.mkIf cfg.publish.enable {
systemd.services.guix-publish = {
description = "Guix remote store";
environment = serviceEnv;
# Mounts will be required by the daemon service anyways so there's no
# need add RequiresMountsFor= or something similar.
requires = [ "guix-daemon.service" ];
after = [ "guix-daemon.service" ];
partOf = [ "guix-daemon.service" ];
preStart = lib.mkIf cfg.publish.generateKeyPair ''
# Generate the keypair if it's missing.
[ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \
${lib.getExe' package "guix"} archive --generate-key || {
rm /etc/guix/signing-key.*;
${lib.getExe' package "guix"} archive --generate-key;
}
'';
script = ''
exec ${lib.getExe' package "guix"} publish \
--user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \
${lib.escapeShellArgs cfg.publish.extraArgs}
'';
serviceConfig = {
Restart = "always";
RestartSec = 10;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
SystemCallFilter = [
"@system-service"
"@debug"
"@setuid"
];
RestrictNamespaces = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
# While the permissions can be set, it is assumed to be taken by Guix
# daemon service which it has already done the setup.
ConfigurationDirectory = "guix";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [
"CAP_NET_BIND_SERVICE"
"CAP_SETUID"
"CAP_SETGID"
];
};
wantedBy = [ "multi-user.target" ];
};
users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") {
description = "Guix publish user";
group = config.users.groups.guix-publish.name;
isSystemUser = true;
};
users.groups.guix-publish = { };
})
(lib.mkIf cfg.gc.enable {
# This service should be handled by root to collect all garbage by all
# users.
systemd.services.guix-gc = {
description = "Guix garbage collection";
startAt = cfg.gc.dates;
script = ''
exec ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs}
'';
serviceConfig = {
Type = "oneshot";
PrivateDevices = true;
PrivateNetwork = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelTunables = true;
SystemCallFilter = [
"@default"
"@file-system"
"@basic-io"
"@system-service"
];
};
};
systemd.timers.guix-gc.timerConfig.Persistent = true;
})
]
);
}

View File

@@ -0,0 +1,91 @@
{
config,
lib,
options,
pkgs,
...
}:
let
name = "headphones";
cfg = config.services.headphones;
opt = options.services.headphones;
in
{
###### interface
options = {
services.headphones = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the headphones server.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/${name}";
description = "Path where to store data files.";
};
configFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/config.ini";
defaultText = lib.literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
description = "Path to config file.";
};
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Host to listen on.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8181;
description = "Port to bind to.";
};
user = lib.mkOption {
type = lib.types.str;
default = name;
description = "User to run the service as";
};
group = lib.mkOption {
type = lib.types.str;
default = name;
description = "Group to run the service as";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.users = lib.optionalAttrs (cfg.user == name) {
${name} = {
uid = config.ids.uids.headphones;
group = cfg.group;
description = "headphones user";
home = cfg.dataDir;
createHome = true;
};
};
users.groups = lib.optionalAttrs (cfg.group == name) {
${name}.gid = config.ids.gids.headphones;
};
systemd.services.headphones = {
description = "Headphones Server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${pkgs.headphones}/bin/headphones --datadir ${cfg.dataDir} --config ${cfg.configFile} --host ${cfg.host} --port ${toString cfg.port}";
};
};
};
}

View File

@@ -0,0 +1,227 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.heisenbridge;
pkg = config.services.heisenbridge.package;
bin = "${pkg}/bin/heisenbridge";
jsonType = (pkgs.formats.json { }).type;
registrationFile = "/var/lib/heisenbridge/registration.yml";
# JSON is a proper subset of YAML
bridgeConfig = builtins.toFile "heisenbridge-registration.yml" (
builtins.toJSON {
id = "heisenbridge";
url = cfg.registrationUrl;
# Don't specify as_token and hs_token
rate_limited = false;
sender_localpart = "heisenbridge";
namespaces = cfg.namespaces;
}
);
in
{
options.services.heisenbridge = {
enable = lib.mkEnableOption "the Matrix to IRC bridge";
package = lib.mkPackageOption pkgs "heisenbridge" { };
homeserver = lib.mkOption {
type = lib.types.str;
description = "The URL to the home server for client-server API calls";
example = "http://localhost:8008";
};
registrationUrl = lib.mkOption {
type = lib.types.str;
description = ''
The URL where the application service is listening for HS requests, from the Matrix HS perspective.#
The default value assumes the bridge runs on the same host as the home server, in the same network.
'';
example = "https://matrix.example.org";
default = "http://${cfg.address}:${toString cfg.port}";
defaultText = "http://$${cfg.address}:$${toString cfg.port}";
};
address = lib.mkOption {
type = lib.types.str;
description = "Address to listen on. IPv6 does not seem to be supported.";
default = "127.0.0.1";
example = "0.0.0.0";
};
port = lib.mkOption {
type = lib.types.port;
description = "The port to listen on";
default = 9898;
};
debug = lib.mkOption {
type = lib.types.bool;
description = "More verbose logging. Recommended during initial setup.";
default = false;
};
owner = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Set owner MXID otherwise first talking local user will claim the bridge
'';
default = null;
example = "@admin:example.org";
};
namespaces = lib.mkOption {
description = "Configure the 'namespaces' section of the registration.yml for the bridge and the server";
# TODO link to Matrix documentation of the format
type = lib.types.submodule {
freeformType = jsonType;
};
default = {
users = [
{
regex = "@irc_.*";
exclusive = true;
}
];
aliases = [ ];
rooms = [ ];
};
};
identd.enable = lib.mkEnableOption "identd service support";
identd.port = lib.mkOption {
type = lib.types.port;
description = "identd listen port";
default = 113;
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Heisenbridge is configured over the command line. Append extra arguments here";
default = [ ];
};
};
config = lib.mkIf cfg.enable {
systemd.services.heisenbridge = {
description = "Matrix<->IRC bridge";
before = [ "matrix-synapse.service" ]; # So the registration file can be used by Synapse
wantedBy = [ "multi-user.target" ];
preStart = ''
umask 077
set -e -u -o pipefail
if ! [ -f "${registrationFile}" ]; then
# Generate registration file if not present (actually, we only care about the tokens in it)
${bin} --generate --config ${registrationFile}
fi
# Overwrite the registration file with our generated one (the config may have changed since then),
# but keep the tokens. Two step procedure to be failure safe
${pkgs.yq}/bin/yq --slurp \
'.[0] + (.[1] | {as_token, hs_token})' \
${bridgeConfig} \
${registrationFile} \
> ${registrationFile}.new
mv -f ${registrationFile}.new ${registrationFile}
# Grant Synapse access to the registration
if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then
chgrp -v matrix-synapse ${registrationFile}
chmod -v g+r ${registrationFile}
fi
'';
serviceConfig = rec {
Type = "simple";
ExecStart = lib.concatStringsSep " " (
[
bin
(if cfg.debug then "-vvv" else "-v")
"--config"
registrationFile
"--listen-address"
(lib.escapeShellArg cfg.address)
"--listen-port"
(toString cfg.port)
]
++ (lib.optionals (cfg.owner != null) [
"--owner"
(lib.escapeShellArg cfg.owner)
])
++ (lib.optionals cfg.identd.enable [
"--identd"
"--identd-port"
(toString cfg.identd.port)
])
++ [
(lib.escapeShellArg cfg.homeserver)
]
++ (map (lib.escapeShellArg) cfg.extraArgs)
);
# Hardening options
User = "heisenbridge";
Group = "heisenbridge";
RuntimeDirectory = "heisenbridge";
RuntimeDirectoryMode = "0700";
StateDirectory = "heisenbridge";
StateDirectoryMode = "0755";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectHostname = true;
ProtectClock = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RemoveIPC = true;
UMask = "0077";
CapabilityBoundingSet = [
"CAP_CHOWN"
]
++ lib.optional (
cfg.port < 1024 || (cfg.identd.enable && cfg.identd.port < 1024)
) "CAP_NET_BIND_SERVICE";
AmbientCapabilities = CapabilityBoundingSet;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
SystemCallFilter = [
"@system-service"
"~@privileged"
"@chown"
];
SystemCallArchitectures = "native";
RestrictAddressFamilies = "AF_INET AF_INET6";
};
};
users.groups.heisenbridge = { };
users.users.heisenbridge = {
description = "Service user for the Heisenbridge";
group = "heisenbridge";
isSystemUser = true;
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,307 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.homepage-dashboard;
# Define the settings format used for this program
settingsFormat = pkgs.formats.yaml { };
in
{
options = {
services.homepage-dashboard = {
enable = lib.mkEnableOption "Homepage Dashboard, a highly customizable application dashboard";
package = lib.mkPackageOption pkgs "homepage-dashboard" { };
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for Homepage.";
};
listenPort = lib.mkOption {
type = lib.types.port;
default = 8082;
description = "Port for Homepage to bind to.";
};
allowedHosts = lib.mkOption {
type = lib.types.str;
default = "localhost:8082,127.0.0.1:8082";
example = "example.com";
description = ''
Hosts that homepage-dashboard will be running under.
You will want to change this in order to acess homepage from anything other than localhost.
see the upsream documentation:
<https://gethomepage.dev/installation/#homepage_allowed_hosts>
'';
};
environmentFile = lib.mkOption {
type = lib.types.str;
description = ''
The path to an environment file that contains environment variables to pass
to the homepage-dashboard service, for the purpose of passing secrets to
the service.
See the upstream documentation:
<https://gethomepage.dev/installation/docker/#using-environment-secrets>
'';
default = "";
};
customCSS = lib.mkOption {
type = lib.types.lines;
description = ''
Custom CSS for styling Homepage.
See <https://gethomepage.dev/configs/custom-css-js/>.
'';
default = "";
};
customJS = lib.mkOption {
type = lib.types.lines;
description = ''
Custom Javascript for Homepage.
See <https://gethomepage.dev/configs/custom-css-js/>.
'';
default = "";
};
bookmarks = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage bookmarks configuration.
See <https://gethomepage.dev/configs/bookmarks/>.
'';
# Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/bookmarks.yaml
example = [
{
Developer = [
{
Github = [
{
abbr = "GH";
href = "https://github.com/";
}
];
}
];
}
{
Entertainment = [
{
YouTube = [
{
abbr = "YT";
href = "https://youtube.com/";
}
];
}
];
}
];
default = [ ];
};
services = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage services configuration.
See <https://gethomepage.dev/configs/services/>.
'';
# Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/services.yaml
example = [
{
"My First Group" = [
{
"My First Service" = {
href = "http://localhost/";
description = "Homepage is awesome";
};
}
];
}
{
"My Second Group" = [
{
"My Second Service" = {
href = "http://localhost/";
description = "Homepage is the best";
};
}
];
}
];
default = [ ];
};
widgets = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage widgets configuration.
See <https://gethomepage.dev/widgets/>.
'';
# Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/widgets.yaml
example = [
{
resources = {
cpu = true;
memory = true;
disk = "/";
};
}
{
search = {
provider = "duckduckgo";
target = "_blank";
};
}
];
default = [ ];
};
kubernetes = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage kubernetes configuration.
See <https://gethomepage.dev/configs/kubernetes/>.
'';
default = { };
};
docker = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage docker configuration.
See <https://gethomepage.dev/configs/docker/>.
'';
default = { };
};
proxmox = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage proxmox configuration.
See <https://gethomepage.dev/configs/proxmox/>.
'';
default = { };
};
settings = lib.mkOption {
inherit (settingsFormat) type;
description = ''
Homepage settings.
See <https://gethomepage.dev/configs/settings/>.
'';
# Defaults: https://github.com/gethomepage/homepage/blob/main/src/skeleton/settings.yaml
default = { };
};
};
};
config = lib.mkIf cfg.enable {
environment.etc = {
"homepage-dashboard/custom.css".text = cfg.customCSS;
"homepage-dashboard/custom.js".text = cfg.customJS;
"homepage-dashboard/bookmarks.yaml".source = settingsFormat.generate "bookmarks.yaml" cfg.bookmarks;
"homepage-dashboard/docker.yaml".source = settingsFormat.generate "docker.yaml" cfg.docker;
"homepage-dashboard/kubernetes.yaml".source =
settingsFormat.generate "kubernetes.yaml" cfg.kubernetes;
"homepage-dashboard/services.yaml".source = settingsFormat.generate "services.yaml" cfg.services;
"homepage-dashboard/settings.yaml".source = settingsFormat.generate "settings.yaml" cfg.settings;
"homepage-dashboard/widgets.yaml".source = settingsFormat.generate "widgets.yaml" cfg.widgets;
"homepage-dashboard/proxmox.yaml".source = settingsFormat.generate "proxmox.yaml" cfg.proxmox;
};
systemd.services.homepage-dashboard = {
description = "Homepage Dashboard";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
HOMEPAGE_CONFIG_DIR = "/etc/homepage-dashboard";
NIXPKGS_HOMEPAGE_CACHE_DIR = "/var/cache/homepage-dashboard";
PORT = toString cfg.listenPort;
LOG_TARGETS = "stdout";
HOMEPAGE_ALLOWED_HOSTS = cfg.allowedHosts;
};
serviceConfig = {
Type = "simple";
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = "homepage-dashboard";
CacheDirectory = "homepage-dashboard";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
# hardening
DynamicUser = true;
DevicePolicy = "closed";
CapabilityBoundingSet = "";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
"AF_NETLINK"
];
DeviceAllow = "";
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
LockPersonality = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
];
ProtectProc = "invisible";
ProtectHostname = true;
UMask = "0077";
# cpu widget requires access to /proc
ProcSubset = if lib.any (widget: widget.resources.cpu or false) cfg.widgets then "all" else "pid";
};
enableStrictShellChecks = true;
# Related:
# * https://github.com/NixOS/nixpkgs/issues/346016 ("homepage-dashboard: cache dir is not cleared upon version upgrade")
# * https://github.com/gethomepage/homepage/discussions/4560 ("homepage NixOS package does not clear cache on upgrade leaving broken state")
# * https://github.com/vercel/next.js/discussions/58864 ("Feature Request: Allow configuration of cache dir")
preStart = ''
rm -rf "''${NIXPKGS_HOMEPAGE_CACHE_DIR:?}"/*
'';
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listenPort ];
};
};
}

View File

@@ -0,0 +1,67 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.ihaskell;
ihaskell = pkgs.ihaskell.override {
packages = cfg.extraPackages;
};
in
{
options = {
services.ihaskell = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Autostart an IHaskell notebook service.";
};
extraPackages = lib.mkOption {
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = haskellPackages: [ ];
defaultText = lib.literalExpression "haskellPackages: []";
example = lib.literalExpression ''
haskellPackages: [
haskellPackages.wreq
haskellPackages.lens
]
'';
description = ''
Extra packages available to ghc when running ihaskell. The
value must be a function which receives the attrset defined
in {var}`haskellPackages` as the sole argument.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.users.ihaskell = {
group = config.users.groups.ihaskell.name;
description = "IHaskell user";
home = "/var/lib/ihaskell";
createHome = true;
uid = config.ids.uids.ihaskell;
};
users.groups.ihaskell.gid = config.ids.gids.ihaskell;
systemd.services.ihaskell = {
description = "IHaskell notebook instance";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = config.users.users.ihaskell.name;
Group = config.users.groups.ihaskell.name;
ExecStart = "${pkgs.runtimeShell} -c \"cd $HOME;${ihaskell}/bin/ihaskell-notebook\"";
};
};
};
}

View File

@@ -0,0 +1,34 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.input-remapper;
in
{
options = {
services.input-remapper = {
enable = lib.mkEnableOption "input-remapper, an easy to use tool to change the mapping of your input device buttons";
package = lib.mkPackageOption pkgs "input-remapper" { };
enableUdevRules = lib.mkEnableOption "udev rules added by input-remapper to handle hotplugged devices. Currently disabled by default due to <https://github.com/sezanzeb/input-remapper/issues/140>";
serviceWantedBy = lib.mkOption {
default = [ "graphical.target" ];
example = [ "multi-user.target" ];
type = lib.types.listOf lib.types.str;
description = "Specifies the WantedBy setting for the input-remapper service.";
};
};
};
config = lib.mkIf cfg.enable {
services.udev.packages = lib.mkIf cfg.enableUdevRules [ cfg.package ];
services.dbus.packages = [ cfg.package ];
systemd.packages = [ cfg.package ];
environment.systemPackages = [ cfg.package ];
systemd.services.input-remapper.wantedBy = cfg.serviceWantedBy;
};
meta.maintainers = with lib.maintainers; [ LunNova ];
}

View File

@@ -0,0 +1,120 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.invidious-router;
settingsFormat = pkgs.formats.yaml { };
configFile = settingsFormat.generate "config.yaml" cfg.settings;
in
{
meta.maintainers = [ lib.maintainers.sils ];
options.services.invidious-router = {
enable = lib.mkEnableOption "the invidious-router service";
port = lib.mkOption {
type = lib.types.port;
default = 8050;
description = ''
Port to bind to.
'';
};
address = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Address on which invidious-router should listen on.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
};
default = {
app = {
listen = "127.0.0.1:8050";
enable_youtube_fallback = false;
reload_instance_list_interval = "60s";
};
api = {
enabled = true;
url = "https://api.invidious.io/instances.json";
filter_regions = true;
allowed_regions = [
"AT"
"DE"
"CH"
];
};
healthcheck = {
path = "/";
allowed_status_codes = [
200
];
timeout = "1s";
interval = "10s";
filter_by_response_time = {
enabled = true;
qty_of_top_results = 3;
};
minimum_ratio = 0.2;
remove_no_ratio = true;
text_not_present = "YouTube is currently trying to block Invidious instances";
};
};
description = ''
Configuration for invidious-router.
Check <https://gitlab.com/gaincoder/invidious-router#configuration>
for configuration options.
'';
};
package = lib.mkPackageOption pkgs "invidious-router" { };
nginx = {
enable = lib.mkEnableOption ''
Automatic nginx proxy configuration
'';
domain = lib.mkOption {
type = lib.types.str;
example = "invidious-router.example.com";
description = ''
The domain on which invidious-router should be served.
'';
};
extraDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Additional domains to serve invidious-router on.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.invidious-router = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
serviceConfig = {
Restart = "on-failure";
ExecStart = "${lib.getExe cfg.package} --configfile ${configFile}";
DynamicUser = "yes";
};
};
services.nginx.virtualHosts = lib.mkIf cfg.nginx.enable {
${cfg.nginx.domain} = {
locations."/" = {
recommendedProxySettings = true;
proxyPass = "http://${cfg.address}:${toString cfg.port}";
};
enableACME = true;
forceSSL = true;
serverAliases = cfg.nginx.extraDomains;
};
};
};
}

View File

@@ -0,0 +1,73 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.irkerd;
ports = [ 6659 ];
in
{
options.services.irkerd = {
enable = lib.mkOption {
description = "Whether to enable irker, an IRC notification daemon.";
default = false;
type = lib.types.bool;
};
openPorts = lib.mkOption {
description = "Open ports in the firewall for irkerd";
default = false;
type = lib.types.bool;
};
listenAddress = lib.mkOption {
default = "localhost";
example = "0.0.0.0";
type = lib.types.str;
description = ''
Specifies the bind address on which the irker daemon listens.
The default is localhost.
Irker authors strongly warn about the risks of running this on
a publicly accessible interface, so change this with caution.
'';
};
nick = lib.mkOption {
default = "irker";
type = lib.types.str;
description = "Nick to use for irker";
};
};
config = lib.mkIf cfg.enable {
systemd.services.irkerd = {
description = "Internet Relay Chat (IRC) notification daemon";
documentation = [
"man:irkerd(8)"
"man:irkerhook(1)"
"man:irk(1)"
];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.irker}/bin/irkerd -H ${cfg.listenAddress} -n ${cfg.nick}";
User = "irkerd";
};
};
environment.systemPackages = [ pkgs.irker ];
users.users.irkerd = {
description = "Irker daemon user";
isSystemUser = true;
group = "irkerd";
};
users.groups.irkerd = { };
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openPorts ports;
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openPorts ports;
};
}

View File

@@ -0,0 +1,136 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.jackett;
in
{
options = {
services.jackett = {
enable = lib.mkEnableOption "Jackett, API support for your favorite torrent trackers";
port = lib.mkOption {
default = 9117;
type = lib.types.port;
description = ''
Port serving the web interface
'';
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/jackett/.config/Jackett";
description = "The directory where Jackett stores its data files.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the Jackett web interface.";
};
user = lib.mkOption {
type = lib.types.str;
default = "jackett";
description = "User account under which Jackett runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "jackett";
description = "Group under which Jackett runs.";
};
package = lib.mkPackageOption pkgs "jackett" { };
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
];
systemd.services.jackett = {
description = "Jackett";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/Jackett --NoUpdates --Port ${toString cfg.port} --DataFolder '${cfg.dataDir}'";
Restart = "on-failure";
# Sandboxing
CapabilityBoundingSet = [
"CAP_NET_BIND_SERVICE"
];
ExecPaths = [
"${builtins.storeDir}"
];
LockPersonality = true;
NoExecPaths = [
"/"
];
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
ReadWritePaths = [
cfg.dataDir
];
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@clock"
"~@cpu-emulation"
"~@debug"
"~@obsolete"
"~@reboot"
"~@module"
"~@mount"
"~@swap"
];
UMask = "0077";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
users.users = lib.mkIf (cfg.user == "jackett") {
jackett = {
group = cfg.group;
home = cfg.dataDir;
uid = config.ids.uids.jackett;
};
};
users.groups = lib.mkIf (cfg.group == "jackett") {
jackett.gid = config.ids.gids.jackett;
};
};
}

View File

@@ -0,0 +1,207 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
mkIf
getExe
maintainers
mkEnableOption
mkOption
mkPackageOption
;
inherit (lib.types) str path bool;
cfg = config.services.jellyfin;
in
{
options = {
services.jellyfin = {
enable = mkEnableOption "Jellyfin Media Server";
package = mkPackageOption pkgs "jellyfin" { };
user = mkOption {
type = str;
default = "jellyfin";
description = "User account under which Jellyfin runs.";
};
group = mkOption {
type = str;
default = "jellyfin";
description = "Group under which jellyfin runs.";
};
dataDir = mkOption {
type = path;
default = "/var/lib/jellyfin";
description = ''
Base data directory,
passed with `--datadir` see [#data-directory](https://jellyfin.org/docs/general/administration/configuration/#data-directory)
'';
};
configDir = mkOption {
type = path;
default = "${cfg.dataDir}/config";
defaultText = "\${cfg.dataDir}/config";
description = ''
Directory containing the server configuration files,
passed with `--configdir` see [configuration-directory](https://jellyfin.org/docs/general/administration/configuration/#configuration-directory)
'';
};
cacheDir = mkOption {
type = path;
default = "/var/cache/jellyfin";
description = ''
Directory containing the jellyfin server cache,
passed with `--cachedir` see [#cache-directory](https://jellyfin.org/docs/general/administration/configuration/#cache-directory)
'';
};
logDir = mkOption {
type = path;
default = "${cfg.dataDir}/log";
defaultText = "\${cfg.dataDir}/log";
description = ''
Directory where the Jellyfin logs will be stored,
passed with `--logdir` see [#log-directory](https://jellyfin.org/docs/general/administration/configuration/#log-directory)
'';
};
openFirewall = mkOption {
type = bool;
default = false;
description = ''
Open the default ports in the firewall for the media server. The
HTTP/HTTPS ports can be changed in the Web UI, so this option should
only be used if they are unchanged, see [Port Bindings](https://jellyfin.org/docs/general/networking/#port-bindings).
'';
};
};
};
config = mkIf cfg.enable {
systemd = {
tmpfiles.settings.jellyfinDirs = {
"${cfg.dataDir}"."d" = {
mode = "700";
inherit (cfg) user group;
};
"${cfg.configDir}"."d" = {
mode = "700";
inherit (cfg) user group;
};
"${cfg.logDir}"."d" = {
mode = "700";
inherit (cfg) user group;
};
"${cfg.cacheDir}"."d" = {
mode = "700";
inherit (cfg) user group;
};
};
services.jellyfin = {
description = "Jellyfin Media Server";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
# This is mostly follows: https://github.com/jellyfin/jellyfin/blob/master/fedora/jellyfin.service
# Upstream also disable some hardenings when running in LXC, we do the same with the isContainer option
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
UMask = "0077";
WorkingDirectory = cfg.dataDir;
ExecStart = "${getExe cfg.package} --datadir '${cfg.dataDir}' --configdir '${cfg.configDir}' --cachedir '${cfg.cacheDir}' --logdir '${cfg.logDir}'";
Restart = "on-failure";
TimeoutSec = 15;
SuccessExitStatus = [
"0"
"143"
];
# Security options:
NoNewPrivileges = true;
SystemCallArchitectures = "native";
# AF_NETLINK needed because Jellyfin monitors the network connection
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = !config.boot.isContainer;
RestrictRealtime = true;
RestrictSUIDSGID = true;
ProtectControlGroups = !config.boot.isContainer;
ProtectHostname = true;
ProtectKernelLogs = !config.boot.isContainer;
ProtectKernelModules = !config.boot.isContainer;
ProtectKernelTunables = !config.boot.isContainer;
LockPersonality = true;
PrivateTmp = !config.boot.isContainer;
# needed for hardware acceleration
PrivateDevices = false;
PrivateUsers = true;
RemoveIPC = true;
SystemCallFilter = [
"~@clock"
"~@aio"
"~@chown"
"~@cpu-emulation"
"~@debug"
"~@keyring"
"~@memlock"
"~@module"
"~@mount"
"~@obsolete"
"~@privileged"
"~@raw-io"
"~@reboot"
"~@setuid"
"~@swap"
];
SystemCallErrorNumber = "EPERM";
};
};
};
users.users = mkIf (cfg.user == "jellyfin") {
jellyfin = {
inherit (cfg) group;
isSystemUser = true;
};
};
users.groups = mkIf (cfg.group == "jellyfin") {
jellyfin = { };
};
networking.firewall = mkIf cfg.openFirewall {
# from https://jellyfin.org/docs/general/networking/index.html
allowedTCPPorts = [
8096
8920
];
allowedUDPPorts = [
1900
7359
];
};
};
meta.maintainers = with maintainers; [
minijackson
fsnkty
];
}

View File

@@ -0,0 +1,73 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.jellyseerr;
in
{
meta.maintainers = [ lib.maintainers.camillemndn ];
options.services.jellyseerr = {
enable = lib.mkEnableOption ''Jellyseerr, a requests manager for Jellyfin'';
package = lib.mkPackageOption pkgs "jellyseerr" { };
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''Open port in the firewall for the Jellyseerr web interface.'';
};
port = lib.mkOption {
type = lib.types.port;
default = 5055;
description = ''The port which the Jellyseerr web UI should listen to.'';
};
configDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/jellyseerr/config";
description = "Config data directory";
};
};
config = lib.mkIf cfg.enable {
systemd.services.jellyseerr = {
description = "Jellyseerr, a requests manager for Jellyfin";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
PORT = toString cfg.port;
CONFIG_DIRECTORY = cfg.configDir;
};
serviceConfig = {
Type = "exec";
StateDirectory = "jellyseerr";
DynamicUser = true;
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
ProtectHome = true;
ProtectSystem = "strict";
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
NoNewPrivileges = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

View File

@@ -0,0 +1,63 @@
# Apache Kafka {#module-services-apache-kafka}
[Apache Kafka](https://kafka.apache.org/) is an open-source distributed event
streaming platform
## Basic Usage {#module-services-apache-kafka-basic-usage}
The Apache Kafka service is configured almost exclusively through its
[settings](#opt-services.apache-kafka.settings) option, with each attribute
corresponding to the [upstream configuration
manual](https://kafka.apache.org/documentation/#configuration) broker settings.
## KRaft {#module-services-apache-kafka-kraft}
Unlike in Zookeeper mode, Kafka in
[KRaft](https://kafka.apache.org/documentation/#kraft) mode requires each log
dir to be "formatted" (which means a cluster-specific a metadata file must
exist in each log dir)
The upstream intention is for users to execute the [storage
tool](https://kafka.apache.org/documentation/#kraft_storage) to achieve this,
but this module contains a few extra options to automate this:
- [](#opt-services.apache-kafka.clusterId)
- [](#opt-services.apache-kafka.formatLogDirs)
- [](#opt-services.apache-kafka.formatLogDirsIgnoreFormatted)
## Migrating to settings {#module-services-apache-kafka-migrating-to-settings}
Migrating a cluster to the new `settings`-based changes requires adapting removed options to the corresponding upstream settings.
This means that the upstream [Broker Configs documentation](https://kafka.apache.org/documentation/#brokerconfigs) should be followed closely.
Note that dotted options in the upstream docs do _not_ correspond to nested Nix attrsets, but instead as quoted top level `settings` attributes, as in `services.apache-kafka.settings."broker.id"`, *NOT* `services.apache-kafka.settings.broker.id`.
Care should be taken, especially when migrating clusters from the old module, to ensure that the same intended configuration is reproduced faithfully via `settings`.
To assist in the comparison, the final config can be inspected by building the config file itself, ie. with: `nix-build <nixpkgs/nixos> -A config.services.apache-kafka.configFiles.serverProperties`.
Notable changes to be aware of include:
- Removal of `services.apache-kafka.extraProperties` and `services.apache-kafka.serverProperties`
- Translate using arbitrary properties using [](#opt-services.apache-kafka.settings)
- [Upstream docs](https://kafka.apache.org/documentation.html#brokerconfigs)
- The intention is for all broker properties to be fully representable via [](#opt-services.apache-kafka.settings).
- If this is not the case, please do consider raising an issue.
- Until it can be remedied, you *can* bail out by using [](#opt-services.apache-kafka.configFiles.serverProperties) to the path of a fully rendered properties file.
- Removal of `services.apache-kafka.hostname` and `services.apache-kafka.port`
- Translate using: `services.apache-kafka.settings.listeners`
- [Upstream docs](https://kafka.apache.org/documentation.html#brokerconfigs_listeners)
- Removal of `services.apache-kafka.logDirs`
- Translate using: `services.apache-kafka.settings."log.dirs"`
- [Upstream docs](https://kafka.apache.org/documentation.html#brokerconfigs_log.dirs)
- Removal of `services.apache-kafka.brokerId`
- Translate using: `services.apache-kafka.settings."broker.id"`
- [Upstream docs](https://kafka.apache.org/documentation.html#brokerconfigs_broker.id)
- Removal of `services.apache-kafka.zookeeper`
- Translate using: `services.apache-kafka.settings."zookeeper.connect"`
- [Upstream docs](https://kafka.apache.org/documentation.html#brokerconfigs_zookeeper.connect)

View File

@@ -0,0 +1,295 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.klipper;
format = pkgs.formats.ini {
# https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
listToValue =
l:
if builtins.length l == 1 then
lib.generators.mkValueStringDefault { } (lib.head l)
else
lib.concatMapStrings (s: "\n ${lib.generators.mkValueStringDefault { } s}") l;
mkKeyValue = lib.generators.mkKeyValueDefault { } ":";
};
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "klipper" "mutableConfigFolder" ]
[ "services" "klipper" "configDir" ]
)
];
##### interface
options = {
services.klipper = {
enable = lib.mkEnableOption "Klipper, the 3D printer firmware";
package = lib.mkPackageOption pkgs "klipper" { };
logFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/klipper/klipper.log";
description = ''
Path of the file Klipper should log to.
If `null`, it logs to stdout, which is not recommended by upstream.
'';
};
inputTTY = lib.mkOption {
type = lib.types.path;
default = "/run/klipper/tty";
description = "Path of the virtual printer symlink to create.";
};
apiSocket = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = "/run/klipper/api";
description = "Path of the API socket to create.";
};
mutableConfig = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Whether to manage the config outside of NixOS.
It will still be initialized with the defined NixOS config if the file doesn't already exist.
'';
};
configDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/klipper";
description = "Path to Klipper config file.";
};
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to default Klipper config.";
};
octoprintIntegration = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Allows Octoprint to control Klipper.";
};
user = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
User account under which Klipper runs.
If null is specified (default), a temporary user will be created by systemd.
'';
};
group = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Group account under which Klipper runs.
If null is specified (default), a temporary user will be created by systemd.
'';
};
settings = lib.mkOption {
type = lib.types.nullOr format.type;
default = null;
description = ''
Configuration for Klipper. See the [documentation](https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides)
for supported values.
'';
};
extraSettings = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra lines to append to the generated Klipper configuration.";
};
firmwares = lib.mkOption {
description = "Firmwares klipper should manage";
default = { };
type =
with lib.types;
attrsOf (submodule {
options = {
enable = lib.mkEnableOption ''
building of firmware for manual flashing
'';
enableKlipperFlash = lib.mkEnableOption ''
flashings scripts for firmware. This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware.
Please check the configs at [klipper](https://github.com/Klipper3d/klipper/tree/master/config) whether your board supports flashing via `make flash`
'';
serial = lib.mkOption {
type = lib.types.nullOr path;
default = null;
description = "Path to serial port this printer is connected to. Leave `null` to derive it from `service.klipper.settings`.";
};
configFile = lib.mkOption {
type = path;
description = "Path to firmware config which is generated using `klipper-genconf`";
};
};
});
};
};
};
##### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
message = "Option services.klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
}
{
assertion = cfg.user != null -> cfg.group != null;
message = "Option services.klipper.group is not set when services.klipper.user is specified.";
}
{
assertion =
cfg.settings != null
-> lib.foldl (a: b: a && b) true (
lib.mapAttrsToList (
mcu: _: mcu != null -> (lib.hasAttrByPath [ "${mcu}" "serial" ] cfg.settings)
) cfg.firmwares
);
message = "Option services.klipper.settings.$mcu.serial must be set when settings.klipper.firmware.$mcu is specified";
}
{
assertion = (cfg.configFile != null) != (cfg.settings != null);
message = "You need to either specify services.klipper.settings or services.klipper.configFile.";
}
{
assertion = (cfg.configFile != null) -> (cfg.extraSettings == "");
message = "You can't use services.klipper.extraSettings with services.klipper.configFile.";
}
];
services.klipper = lib.mkIf cfg.octoprintIntegration {
user = config.services.octoprint.user;
group = config.services.octoprint.group;
};
systemd.services.klipper =
let
klippyArgs =
"--input-tty=${cfg.inputTTY}"
+ lib.optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}"
+ lib.optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}";
printerConfig =
if cfg.settings != null then
builtins.toFile "klipper.cfg" ((format.generate "" cfg.settings).text + cfg.extraSettings)
else
cfg.configFile;
in
{
description = "Klipper 3D Printer Firmware";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
mkdir -p ${cfg.configDir}
pushd ${cfg.configDir}
if [ -e printer.cfg ]; then
${
if cfg.mutableConfig then
":"
else
''
# Backup existing config using the same date format klipper uses for SAVE_CONFIG
old_config="printer-$(date +"%Y%m%d_%H%M%S").cfg"
mv printer.cfg "$old_config"
# Preserve SAVE_CONFIG section from the existing config
cat ${printerConfig} <(printf "\n") <(sed -n '/#*# <---------------------- SAVE_CONFIG ---------------------->/,$p' "$old_config") > printer.cfg
${pkgs.diffutils}/bin/cmp printer.cfg "$old_config" && rm "$old_config"
''
}
else
cat ${printerConfig} > printer.cfg
fi
popd
'';
restartTriggers = lib.optional (!cfg.mutableConfig) [ printerConfig ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${cfg.configDir}/printer.cfg";
RuntimeDirectory = "klipper";
StateDirectory = "klipper";
SupplementaryGroups = [ "dialout" ];
WorkingDirectory = "${cfg.package}/lib";
OOMScoreAdjust = "-999";
CPUSchedulingPolicy = "rr";
CPUSchedulingPriority = 99;
IOSchedulingClass = "realtime";
IOSchedulingPriority = 0;
UMask = "0002";
}
// (
if cfg.user != null then
{
Group = cfg.group;
User = cfg.user;
}
else
{
DynamicUser = true;
User = "klipper";
}
);
};
environment.systemPackages =
let
default = a: b: if a != null then a else b;
genconf = pkgs.klipper-genconf.override {
klipper = cfg.package;
};
firmwares = lib.filterAttrs (n: v: v != null) (
lib.mapAttrs (
mcu:
{
enable,
enableKlipperFlash,
configFile,
serial,
}:
if enable then
pkgs.klipper-firmware.override {
klipper = cfg.package;
mcu = lib.strings.sanitizeDerivationName mcu;
firmwareConfig = configFile;
}
else
null
) cfg.firmwares
);
firmwareFlasher = lib.mapAttrsToList (
mcu: firmware:
pkgs.klipper-flash.override {
klipper = cfg.package;
klipper-firmware = firmware;
mcu = lib.strings.sanitizeDerivationName mcu;
flashDevice = default cfg.firmwares."${mcu}".serial cfg.settings."${mcu}".serial;
firmwareConfig = cfg.firmwares."${mcu}".configFile;
}
) (lib.filterAttrs (mcu: firmware: cfg.firmwares."${mcu}".enableKlipperFlash) firmwares);
in
[ genconf ] ++ firmwareFlasher ++ lib.attrValues firmwares;
};
meta.maintainers = [
lib.maintainers.cab404
];
}

View File

@@ -0,0 +1,104 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.languagetool;
settingsFormat = pkgs.formats.javaProperties { };
in
{
options.services.languagetool = {
enable = lib.mkEnableOption "the LanguageTool server, a multilingual spelling, style, and grammar checker that helps correct or paraphrase texts";
package = lib.mkPackageOption pkgs "languagetool" { };
port = lib.mkOption {
type = lib.types.port;
default = 8081;
example = 8081;
description = ''
Port on which LanguageTool listens.
'';
};
public = lib.mkEnableOption "access from anywhere (rather than just localhost)";
allowOrigin = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://my-website.org";
description = ''
Set the Access-Control-Allow-Origin header in the HTTP response,
used for direct (non-proxy) JavaScript-based access from browsers.
`"*"` to allow access from all sites.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.cacheSize = lib.mkOption {
type = lib.types.ints.unsigned;
default = 1000;
apply = toString;
description = "Number of sentences cached.";
};
};
default = { };
description = ''
Configuration file options for LanguageTool, see
'languagetool-http-server --help'
for supported settings.
'';
};
jrePackage = lib.mkPackageOption pkgs "jre" { };
jvmOptions = lib.mkOption {
description = ''
Extra command line options for the JVM running languagetool.
More information can be found here: <https://docs.oracle.com/en/java/javase/19/docs/specs/man/java.html#standard-options-for-java>
'';
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"-Xmx512m"
];
};
};
config = lib.mkIf cfg.enable {
systemd.services.languagetool = {
description = "LanguageTool HTTP server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
User = "languagetool";
Group = "languagetool";
CapabilityBoundingSet = [ "" ];
RestrictNamespaces = [ "" ];
SystemCallFilter = [
"@system-service"
"~ @privileged"
];
ProtectHome = "yes";
Restart = "on-failure";
ExecStart = ''
${cfg.jrePackage}/bin/java \
-cp ${cfg.package}/share/languagetool-server.jar \
${toString cfg.jvmOptions} \
org.languagetool.server.HTTPServer \
--port ${toString cfg.port} \
${lib.optionalString cfg.public "--public"} \
${lib.optionalString (cfg.allowOrigin != null) "--allow-origin ${cfg.allowOrigin}"} \
"--config" ${settingsFormat.generate "languagetool.conf" cfg.settings}
'';
};
};
};
}

View File

@@ -0,0 +1,64 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.leaps;
stateDir = "/var/lib/leaps/";
in
{
options = {
services.leaps = {
enable = lib.mkEnableOption "leaps, a pair programming service";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "A port where leaps listens for incoming http requests";
};
address = lib.mkOption {
default = "";
type = lib.types.str;
example = "127.0.0.1";
description = "Hostname or IP-address to listen to. By default it will listen on all interfaces.";
};
path = lib.mkOption {
default = "/";
type = lib.types.path;
description = "Subdirectory used for reverse proxy setups";
};
};
};
config = lib.mkIf cfg.enable {
users = {
users.leaps = {
uid = config.ids.uids.leaps;
description = "Leaps server user";
group = "leaps";
home = stateDir;
createHome = true;
};
groups.leaps = {
gid = config.ids.gids.leaps;
};
};
systemd.services.leaps = {
description = "leaps service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = "leaps";
Group = "leaps";
Restart = "on-failure";
WorkingDirectory = stateDir;
PrivateTmp = true;
ExecStart = "${pkgs.leaps}/bin/leaps -path ${toString cfg.path} -address ${cfg.address}:${toString cfg.port}";
};
};
};
}

View File

@@ -0,0 +1,174 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.lifecycled;
# TODO: Add the ability to extend this with an rfc 42-like interface.
# In the meantime, one can modify the environment (as
# long as it's not overriding anything from here) with
# systemd.services.lifecycled.serviceConfig.Environment
configFile = pkgs.writeText "lifecycled" ''
LIFECYCLED_HANDLER=${cfg.handler}
${lib.optionalString (
cfg.cloudwatchGroup != null
) "LIFECYCLED_CLOUDWATCH_GROUP=${cfg.cloudwatchGroup}"}
${lib.optionalString (
cfg.cloudwatchStream != null
) "LIFECYCLED_CLOUDWATCH_STREAM=${cfg.cloudwatchStream}"}
${lib.optionalString cfg.debug "LIFECYCLED_DEBUG=${lib.boolToString cfg.debug}"}
${lib.optionalString (cfg.instanceId != null) "LIFECYCLED_INSTANCE_ID=${cfg.instanceId}"}
${lib.optionalString cfg.json "LIFECYCLED_JSON=${lib.boolToString cfg.json}"}
${lib.optionalString cfg.noSpot "LIFECYCLED_NO_SPOT=${lib.boolToString cfg.noSpot}"}
${lib.optionalString (cfg.snsTopic != null) "LIFECYCLED_SNS_TOPIC=${cfg.snsTopic}"}
${lib.optionalString (cfg.awsRegion != null) "AWS_REGION=${cfg.awsRegion}"}
'';
in
{
meta.maintainers = with lib.maintainers; [
cole-h
grahamc
];
options = {
services.lifecycled = {
enable = lib.mkEnableOption "lifecycled, a daemon for responding to AWS AutoScaling Lifecycle Hooks";
queueCleaner = {
enable = lib.mkEnableOption "lifecycled-queue-cleaner";
frequency = lib.mkOption {
type = lib.types.str;
default = "hourly";
description = ''
How often to trigger the queue cleaner.
NOTE: This string should be a valid value for a systemd
timer's `OnCalendar` configuration. See
{manpage}`systemd.timer(5)`
for more information.
'';
};
parallel = lib.mkOption {
type = lib.types.ints.unsigned;
default = 20;
description = ''
The number of parallel deletes to run.
'';
};
};
instanceId = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The instance ID to listen for events for.
'';
};
snsTopic = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The SNS topic that receives events.
'';
};
noSpot = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Disable the spot termination listener.
'';
};
handler = lib.mkOption {
type = lib.types.path;
description = ''
The script to invoke to handle events.
'';
};
json = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable JSON logging.
'';
};
cloudwatchGroup = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Write logs to a specific Cloudwatch Logs group.
'';
};
cloudwatchStream = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Write logs to a specific Cloudwatch Logs stream. Defaults to the instance ID.
'';
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable debugging information.
'';
};
# XXX: Can be removed if / when
# https://github.com/buildkite/lifecycled/pull/91 is merged.
awsRegion = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The region used for accessing AWS services.
'';
};
};
};
### Implementation ###
config = lib.mkMerge [
(lib.mkIf cfg.enable {
environment.etc."lifecycled".source = configFile;
systemd.packages = [ pkgs.lifecycled ];
systemd.services.lifecycled = {
wantedBy = [ "network-online.target" ];
restartTriggers = [ configFile ];
};
})
(lib.mkIf cfg.queueCleaner.enable {
systemd.services.lifecycled-queue-cleaner = {
description = "Lifecycle Daemon Queue Cleaner";
environment = lib.optionalAttrs (cfg.awsRegion != null) { AWS_REGION = cfg.awsRegion; };
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.lifecycled}/bin/lifecycled-queue-cleaner -parallel ${toString cfg.queueCleaner.parallel}";
};
};
systemd.timers.lifecycled-queue-cleaner = {
description = "Lifecycle Daemon Queue Cleaner Timer";
wantedBy = [ "timers.target" ];
after = [ "network-online.target" ];
timerConfig = {
Unit = "lifecycled-queue-cleaner.service";
OnCalendar = "${cfg.queueCleaner.frequency}";
};
};
})
];
}

View File

@@ -0,0 +1,182 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
cfg = config.services.litellm;
settingsFormat = pkgs.formats.yaml { };
in
{
options = {
services.litellm = {
enable = lib.mkEnableOption "LiteLLM server";
package = lib.mkPackageOption pkgs "litellm" { };
stateDir = lib.mkOption {
type = types.path;
default = "/var/lib/litellm";
example = "/home/foo";
description = "State directory of LiteLLM.";
};
host = lib.mkOption {
type = types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = ''
The host address which the LiteLLM server HTTP interface listens to.
'';
};
port = lib.mkOption {
type = types.port;
default = 8080;
example = 11111;
description = ''
Which port the LiteLLM server listens to.
'';
};
settings = lib.mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
model_list = lib.mkOption {
type = settingsFormat.type;
description = ''
List of supported models on the server, with model-specific configs.
'';
default = [ ];
};
router_settings = lib.mkOption {
type = settingsFormat.type;
description = ''
LiteLLM Router settings
'';
default = { };
};
litellm_settings = lib.mkOption {
type = settingsFormat.type;
description = ''
LiteLLM Module settings
'';
default = { };
};
general_settings = lib.mkOption {
type = settingsFormat.type;
description = ''
LiteLLM Server settings
'';
default = { };
};
environment_variables = lib.mkOption {
type = settingsFormat.type;
description = ''
Environment variables to pass to the Lite
'';
default = { };
};
};
};
default = { };
description = ''
Configuration for LiteLLM.
See <https://docs.litellm.ai/docs/proxy/configs> for more.
'';
};
environment = lib.mkOption {
type = types.attrsOf types.str;
default = {
SCARF_NO_ANALYTICS = "True";
DO_NOT_TRACK = "True";
ANONYMIZED_TELEMETRY = "False";
};
example = ''
{
NO_DOCS="True";
}
'';
description = ''
Extra environment variables for LiteLLM.
'';
};
environmentFile = lib.mkOption {
description = ''
Environment file to be passed to the systemd service.
Useful for passing secrets to the service to prevent them from being
world-readable in the Nix store.
'';
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/secrets/liteLLMSecrets";
};
openFirewall = lib.mkOption {
type = types.bool;
default = false;
description = ''
Whether to open the firewall for LiteLLM.
This adds `services.litellm.port` to `networking.firewall.allowedTCPPorts`.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.litellm = {
description = "LLM Gateway to provide model access, fallbacks and spend tracking across 100+ LLMs.";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = cfg.environment;
serviceConfig =
let
configFile = settingsFormat.generate "config.yaml" cfg.settings;
in
{
ExecStart = "${lib.getExe cfg.package} --host \"${cfg.host}\" --port ${toString cfg.port} --config ${configFile}";
EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
WorkingDirectory = cfg.stateDir;
StateDirectory = "litellm";
RuntimeDirectory = "litellm";
RuntimeDirectoryMode = "0755";
PrivateTmp = true;
DynamicUser = true;
DevicePolicy = "closed";
LockPersonality = true;
PrivateUsers = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
UMask = "0077";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
ProtectClock = true;
ProtectProc = "invisible";
};
};
networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,124 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.llama-cpp;
in
{
options = {
services.llama-cpp = {
enable = lib.mkEnableOption "LLaMA C++ server";
package = lib.mkPackageOption pkgs "llama-cpp" { };
model = lib.mkOption {
type = lib.types.path;
example = "/models/mistral-instruct-7b/ggml-model-q4_0.gguf";
description = "Model path.";
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Extra flags passed to llama-cpp-server.";
example = [
"-c"
"4096"
"-ngl"
"32"
"--numa"
"numactl"
];
default = [ ];
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "IP address the LLaMA C++ server listens on.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Listen port for LLaMA C++ server.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for LLaMA C++ server.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.llama-cpp = {
description = "LLaMA C++ server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "idle";
KillSignal = "SIGINT";
ExecStart = "${cfg.package}/bin/llama-server --log-disable --host ${cfg.host} --port ${builtins.toString cfg.port} -m ${cfg.model} ${utils.escapeSystemdExecArgs cfg.extraFlags}";
Restart = "on-failure";
RestartSec = 300;
# for GPU acceleration
PrivateDevices = false;
# hardening
DynamicUser = true;
CapabilityBoundingSet = "";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
NoNewPrivileges = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
MemoryDenyWriteExecute = true;
LockPersonality = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
SystemCallErrorNumber = "EPERM";
ProtectProc = "invisible";
ProtectHostname = true;
ProcSubset = "pid";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
meta.maintainers = with lib.maintainers; [ newam ];
}

View File

@@ -0,0 +1,35 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.logkeys;
in
{
options.services.logkeys = {
enable = lib.mkEnableOption "logkeys, a keylogger service";
device = lib.mkOption {
description = "Use the given device as keyboard input event device instead of /dev/input/eventX default.";
default = null;
type = lib.types.nullOr lib.types.str;
example = "/dev/input/event15";
};
};
config = lib.mkIf cfg.enable {
systemd.services.logkeys = {
description = "LogKeys Keylogger Daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.logkeys}/bin/logkeys -s${
lib.optionalString (cfg.device != null) " -d ${cfg.device}"
}";
ExecStop = "${pkgs.logkeys}/bin/logkeys -k";
Type = "forking";
};
};
};
}

View File

@@ -0,0 +1,71 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mame;
mame = "mame${lib.optionalString pkgs.stdenv.hostPlatform.is64bit "64"}";
in
{
options = {
services.mame = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to setup TUN/TAP Ethernet interface for MAME emulator.
'';
};
user = lib.mkOption {
type = lib.types.str;
description = ''
User from which you run MAME binary.
'';
};
hostAddr = lib.mkOption {
type = lib.types.str;
description = ''
IP address of the host system. Usually an address of the main network
adapter or the adapter through which you get an internet connection.
'';
example = "192.168.31.156";
};
emuAddr = lib.mkOption {
type = lib.types.str;
description = ''
IP address of the guest system. The same you set inside guest OS under
MAME. Should be on the same subnet as {option}`services.mame.hostAddr`.
'';
example = "192.168.31.155";
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.mame ];
security.wrappers."${mame}" = {
owner = "root";
group = "root";
capabilities = "cap_net_admin,cap_net_raw+eip";
source = "${pkgs.mame}/bin/${mame}";
};
systemd.services.mame = {
description = "MAME TUN/TAP Ethernet interface";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.iproute2 ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.mame}/bin/taputil.sh -c ${cfg.user} ${cfg.emuAddr} ${cfg.hostAddr} -";
ExecStop = "${pkgs.mame}/bin/taputil.sh -d ${cfg.user}";
};
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,114 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mbpfan;
verbose = lib.optionalString cfg.verbose "v";
format = pkgs.formats.ini { };
cfgfile = format.generate "mbpfan.ini" cfg.settings;
in
{
options.services.mbpfan = {
enable = lib.mkEnableOption "mbpfan, fan controller daemon for Apple Macs and MacBooks";
package = lib.mkPackageOption pkgs "mbpfan" { };
verbose = lib.mkOption {
type = lib.types.bool;
default = false;
description = "If true, sets the log level to verbose.";
};
aggressive = lib.mkOption {
type = lib.types.bool;
default = true;
description = "If true, favors higher default fan speeds.";
};
settings = lib.mkOption {
default = { };
description = "INI configuration for Mbpfan.";
type = lib.types.submodule {
freeformType = format.type;
options.general.low_temp = lib.mkOption {
type = lib.types.int;
default = (if cfg.aggressive then 55 else 63);
defaultText = lib.literalExpression "55";
description = "If temperature is below this, fans will run at minimum speed.";
};
options.general.high_temp = lib.mkOption {
type = lib.types.int;
default = (if cfg.aggressive then 58 else 66);
defaultText = lib.literalExpression "58";
description = "If temperature is above this, fan speed will gradually increase.";
};
options.general.max_temp = lib.mkOption {
type = lib.types.int;
default = (if cfg.aggressive then 78 else 86);
defaultText = lib.literalExpression "78";
description = "If temperature is above this, fans will run at maximum speed.";
};
options.general.polling_interval = lib.mkOption {
type = lib.types.int;
default = 1;
description = "The polling interval.";
};
};
};
};
imports = [
(lib.mkRenamedOptionModule
[ "services" "mbpfan" "pollingInterval" ]
[ "services" "mbpfan" "settings" "general" "polling_interval" ]
)
(lib.mkRenamedOptionModule
[ "services" "mbpfan" "maxTemp" ]
[ "services" "mbpfan" "settings" "general" "max_temp" ]
)
(lib.mkRenamedOptionModule
[ "services" "mbpfan" "lowTemp" ]
[ "services" "mbpfan" "settings" "general" "low_temp" ]
)
(lib.mkRenamedOptionModule
[ "services" "mbpfan" "highTemp" ]
[ "services" "mbpfan" "settings" "general" "high_temp" ]
)
(lib.mkRenamedOptionModule
[ "services" "mbpfan" "minFanSpeed" ]
[ "services" "mbpfan" "settings" "general" "min_fan1_speed" ]
)
(lib.mkRenamedOptionModule
[ "services" "mbpfan" "maxFanSpeed" ]
[ "services" "mbpfan" "settings" "general" "max_fan1_speed" ]
)
];
config = lib.mkIf cfg.enable {
boot.kernelModules = [
"coretemp"
"applesmc"
];
environment.systemPackages = [ cfg.package ];
environment.etc."mbpfan.conf".source = cfgfile;
systemd.services.mbpfan = {
description = "A fan manager daemon for MacBook Pro";
wantedBy = [ "sysinit.target" ];
after = [ "sysinit.target" ];
restartTriggers = [ config.environment.etc."mbpfan.conf".source ];
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/mbpfan -f${verbose}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
PIDFile = "/run/mbpfan.pid";
Restart = "always";
};
};
};
}

View File

@@ -0,0 +1,418 @@
{
config,
lib,
options,
pkgs,
...
}:
let
gid = config.ids.gids.mediatomb;
cfg = config.services.mediatomb;
opt = options.services.mediatomb;
name = cfg.package.pname;
pkg = cfg.package;
optionYesNo = option: if option then "yes" else "no";
# configuration on media directory
mediaDirectory = {
options = {
path = lib.mkOption {
type = lib.types.str;
description = ''
Absolute directory path to the media directory to index.
'';
};
recursive = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the indexation must take place recursively or not.";
};
hidden-files = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to index the hidden files or not.";
};
};
};
toMediaDirectory =
d:
"<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
transcodingConfig =
if cfg.transcoding then
with pkgs;
''
<transcoding enabled="yes">
<mimetype-profile-mappings>
<transcode mimetype="video/x-flv" using="vlcmpeg" />
<transcode mimetype="application/ogg" using="vlcmpeg" />
<transcode mimetype="audio/ogg" using="ogg2mp3" />
</mimetype-profile-mappings>
<profiles>
<profile name="ogg2mp3" enabled="no" type="external">
<mimetype>audio/mpeg</mimetype>
<accept-url>no</accept-url>
<first-resource>yes</first-resource>
<accept-ogg-theora>no</accept-ogg-theora>
<agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
<buffer size="1048576" chunk-size="131072" fill-size="262144" />
</profile>
<profile name="vlcmpeg" enabled="no" type="external">
<mimetype>video/mpeg</mimetype>
<accept-url>yes</accept-url>
<first-resource>yes</first-resource>
<accept-ogg-theora>yes</accept-ogg-theora>
<agent command="${lib.getExe vlc}"
arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit" />
<buffer size="14400000" chunk-size="512000" fill-size="120000" />
</profile>
</profiles>
</transcoding>
''
else
''
<transcoding enabled="no">
</transcoding>
'';
configText = lib.optionalString (!cfg.customCfg) ''
<?xml version="1.0" encoding="UTF-8"?>
<config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
<server>
<ui enabled="yes" show-tooltips="yes">
<accounts enabled="no" session-timeout="30">
<account user="${name}" password="${name}"/>
</accounts>
</ui>
<name>${cfg.serverName}</name>
<udn>uuid:${cfg.uuid}</udn>
<home>${cfg.dataDir}</home>
<interface>${cfg.interface}</interface>
<webroot>${pkg}/share/${name}/web</webroot>
<pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
<storage>
<sqlite3 enabled="yes">
<database-file>${name}.db</database-file>
</sqlite3>
</storage>
<protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
${lib.optionalString cfg.dsmSupport ''
<custom-http-headers>
<add header="X-User-Agent: redsonic"/>
</custom-http-headers>
<manufacturerURL>redsonic.com</manufacturerURL>
<modelNumber>105</modelNumber>
''}
${lib.optionalString cfg.tg100Support ''
<upnp-string-limit>101</upnp-string-limit>
''}
<extended-runtime-options>
<mark-played-items enabled="yes" suppress-cds-updates="yes">
<string mode="prepend">*</string>
<mark>
<content>video</content>
</mark>
</mark-played-items>
</extended-runtime-options>
</server>
<import hidden-files="no">
<autoscan use-inotify="auto">
${lib.concatMapStrings toMediaDirectory cfg.mediaDirectories}
</autoscan>
<scripting script-charset="UTF-8">
<common-script>${pkg}/share/${name}/js/common.js</common-script>
<playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
<virtual-layout type="builtin">
<import-script>${pkg}/share/${name}/js/import.js</import-script>
</virtual-layout>
</scripting>
<mappings>
<extension-mimetype ignore-unknown="no">
<map from="mp3" to="audio/mpeg"/>
<map from="ogx" to="application/ogg"/>
<map from="ogv" to="video/ogg"/>
<map from="oga" to="audio/ogg"/>
<map from="ogg" to="audio/ogg"/>
<map from="ogm" to="video/ogg"/>
<map from="asf" to="video/x-ms-asf"/>
<map from="asx" to="video/x-ms-asf"/>
<map from="wma" to="audio/x-ms-wma"/>
<map from="wax" to="audio/x-ms-wax"/>
<map from="wmv" to="video/x-ms-wmv"/>
<map from="wvx" to="video/x-ms-wvx"/>
<map from="wm" to="video/x-ms-wm"/>
<map from="wmx" to="video/x-ms-wmx"/>
<map from="m3u" to="audio/x-mpegurl"/>
<map from="pls" to="audio/x-scpls"/>
<map from="flv" to="video/x-flv"/>
<map from="mkv" to="video/x-matroska"/>
<map from="mka" to="audio/x-matroska"/>
${lib.optionalString cfg.ps3Support ''
<map from="avi" to="video/divx"/>
''}
${lib.optionalString cfg.dsmSupport ''
<map from="avi" to="video/avi"/>
''}
</extension-mimetype>
<mimetype-upnpclass>
<map from="audio/*" to="object.item.audioItem.musicTrack"/>
<map from="video/*" to="object.item.videoItem"/>
<map from="image/*" to="object.item.imageItem"/>
</mimetype-upnpclass>
<mimetype-contenttype>
<treat mimetype="audio/mpeg" as="mp3"/>
<treat mimetype="application/ogg" as="ogg"/>
<treat mimetype="audio/ogg" as="ogg"/>
<treat mimetype="audio/x-flac" as="flac"/>
<treat mimetype="audio/x-ms-wma" as="wma"/>
<treat mimetype="audio/x-wavpack" as="wv"/>
<treat mimetype="image/jpeg" as="jpg"/>
<treat mimetype="audio/x-mpegurl" as="playlist"/>
<treat mimetype="audio/x-scpls" as="playlist"/>
<treat mimetype="audio/x-wav" as="pcm"/>
<treat mimetype="audio/L16" as="pcm"/>
<treat mimetype="video/x-msvideo" as="avi"/>
<treat mimetype="video/mp4" as="mp4"/>
<treat mimetype="audio/mp4" as="mp4"/>
<treat mimetype="application/x-iso9660" as="dvd"/>
<treat mimetype="application/x-iso9660-image" as="dvd"/>
</mimetype-contenttype>
</mappings>
<online-content>
<YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
<favorites user="${name}"/>
<standardfeed feed="most_viewed" time-range="today"/>
<playlists user="${name}"/>
<uploads user="${name}"/>
<standardfeed feed="recently_featured" time-range="today"/>
</YouTube>
</online-content>
</import>
${transcodingConfig}
</config>
'';
defaultFirewallRules = {
# udp 1900 port needs to be opened for SSDP (not configurable within
# mediatomb/gerbera) cf.
# https://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
allowedUDPPorts = [
1900
cfg.port
];
allowedTCPPorts = [ cfg.port ];
};
in
{
###### interface
options = {
services.mediatomb = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the Gerbera/Mediatomb DLNA server.
'';
};
serverName = lib.mkOption {
type = lib.types.str;
default = "Gerbera (Mediatomb)";
description = ''
How to identify the server on the network.
'';
};
package = lib.mkPackageOption pkgs "gerbera" { };
ps3Support = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable ps3 specific tweaks.
WARNING: incompatible with DSM 320 support.
'';
};
dsmSupport = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable D-Link DSM 320 specific tweaks.
WARNING: incompatible with ps3 support.
'';
};
tg100Support = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable Telegent TG100 specific tweaks.
'';
};
transcoding = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable transcoding.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/${name}";
defaultText = lib.literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
description = ''
The directory where Gerbera/Mediatomb stores its state, data, etc.
'';
};
pcDirectoryHide = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to list the top-level directory or not (from upnp client standpoint).
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "mediatomb";
description = "User account under which the service runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "mediatomb";
description = "Group account under which the service runs.";
};
port = lib.mkOption {
type = lib.types.port;
default = 49152;
description = ''
The network port to listen on.
'';
};
interface = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
A specific interface to bind to.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If false (the default), this is up to the user to declare the firewall rules.
If true, this opens port 1900 (tcp and udp) and the port specified by
{option}`sercvices.mediatomb.port`.
If the option {option}`services.mediatomb.interface` is set,
the firewall rules opened are dedicated to that interface. Otherwise,
those rules are opened globally.
'';
};
uuid = lib.mkOption {
type = lib.types.str;
default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
description = ''
A unique (on your network) to identify the server by.
'';
};
mediaDirectories = lib.mkOption {
type = with lib.types; listOf (submodule mediaDirectory);
default = [ ];
description = ''
Declare media directories to index.
'';
example = [
{
path = "/data/pictures";
recursive = false;
hidden-files = false;
}
{
path = "/data/audio";
recursive = true;
hidden-files = false;
}
];
};
customCfg = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow the service to create and use its own config file inside the `dataDir` as
configured by {option}`services.mediatomb.dataDir`.
Deactivated by default, the service then runs with the configuration generated from this module.
Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
config.xml within the configured `dataDir`. It's up to the user to make a correct
configuration file.
'';
};
};
};
###### implementation
config =
let
binaryCommand = "${pkg}/bin/${name}";
interfaceFlag = lib.optionalString (cfg.interface != "") "--interface ${cfg.interface}";
configFlag = lib.optionalString (
!cfg.customCfg
) "--config ${pkgs.writeText "config.xml" configText}";
in
lib.mkIf cfg.enable {
systemd.services.mediatomb = {
description = "${cfg.serverName} media Server";
# Gerbera might fail if the network interface is not available on startup
# https://github.com/gerbera/gerbera/issues/1324
wants = [ "network-online.target" ];
after = [
"network.target"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
serviceConfig.User = cfg.user;
serviceConfig.Group = cfg.group;
};
users.groups = lib.optionalAttrs (cfg.group == "mediatomb") {
mediatomb.gid = gid;
};
users.users = lib.optionalAttrs (cfg.user == "mediatomb") {
mediatomb = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
createHome = true;
description = "${name} DLNA Server User";
};
};
# Open firewall only if users enable it
networking.firewall = lib.mkMerge [
(lib.mkIf (cfg.openFirewall && cfg.interface != "") {
interfaces."${cfg.interface}" = defaultFirewallRules;
})
(lib.mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
];
};
}

View File

@@ -0,0 +1,199 @@
{
config,
options,
pkgs,
lib,
...
}:
let
cfg = config.services.memos;
opt = options.services.memos;
envFileFormat = pkgs.formats.keyValue { };
in
{
options.services.memos = {
enable = lib.mkEnableOption "Memos note-taking";
package = lib.mkPackageOption pkgs "Memos" {
default = "memos";
};
openFirewall = lib.mkEnableOption "opening the ports in the firewall";
user = lib.mkOption {
type = lib.types.str;
description = ''
The user to run Memos as.
::: {.note}
If changing the default value, **you** are responsible of creating the corresponding user with [{option}`users.users`](#opt-users.users).
:::
'';
default = "memos";
};
group = lib.mkOption {
type = lib.types.str;
description = ''
The group to run Memos as.
::: {.note}
If changing the default value, **you** are responsible of creating the corresponding group with [{option}`users.groups`](#opt-users.groups).
:::
'';
default = "memos";
};
dataDir = lib.mkOption {
default = "/var/lib/memos/";
type = lib.types.path;
description = ''
Specifies the directory where Memos will store its data.
::: {.note}
It will be automatically created with the permissions of [{option}`services.memos.user`](#opt-services.memos.user) and [{option}`services.memos.group`](#opt-services.memos.group).
:::
'';
};
settings = lib.mkOption {
type = envFileFormat.type;
description = ''
The environment variables to configure Memos.
::: {.note}
At time of writing, there is no clear documentation about possible values.
It's possible to convert CLI flags into these variables.
Example : CLI flag "--unix-sock" converts to {env}`MEMOS_UNIX_SOCK`.
:::
'';
default = {
MEMOS_MODE = "prod";
MEMOS_ADDR = "127.0.0.1";
MEMOS_PORT = "5230";
MEMOS_DATA = cfg.dataDir;
MEMOS_DRIVER = "sqlite";
MEMOS_INSTANCE_URL = "http://localhost:5230";
};
defaultText = lib.literalExpression ''
{
MEMOS_MODE = "prod";
MEMOS_ADDR = "127.0.0.1";
MEMOS_PORT = "5230";
MEMOS_DATA = config.${opt.dataDir};
MEMOS_DRIVER = "sqlite";
MEMOS_INSTANCE_URL = "http://localhost:5230";
}
'';
};
environmentFile = lib.mkOption {
type = lib.types.path;
description = ''
The environment file to use when starting Memos.
::: {.note}
By default, generated from [](opt-${opt.settings}).
:::
'';
example = "/var/lib/memos/memos.env";
default = envFileFormat.generate "memos.env" cfg.settings;
defaultText = lib.literalMD ''
generated from {option}`${opt.settings}`
'';
};
};
config = lib.mkIf cfg.enable {
users.users = lib.mkIf (cfg.user == "memos") {
${cfg.user} = {
description = lib.mkDefault "Memos service user";
isSystemUser = true;
group = cfg.group;
};
};
users.groups = lib.mkIf (cfg.group == "memos") {
${cfg.group} = { };
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
cfg.port
];
systemd.tmpfiles.settings."10-memos" = {
"${cfg.dataDir}" = {
d = {
mode = "0750";
user = cfg.user;
group = cfg.group;
};
};
};
systemd.services.memos = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
wants = [ "network.target" ];
description = "Memos, a privacy-first, lightweight note-taking solution";
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Type = "simple";
RestartSec = 60;
LimitNOFILE = 65536;
NoNewPrivileges = true;
LockPersonality = true;
RemoveIPC = true;
ReadWritePaths = [
cfg.dataDir
];
ProtectSystem = "strict";
PrivateUsers = true;
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
UMask = "0077";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
SystemCallFilter = [
" " # This is needed to clear the SystemCallFilter existing definitions
"~@reboot"
"~@swap"
"~@obsolete"
"~@mount"
"~@module"
"~@debug"
"~@cpu-emulation"
"~@clock"
"~@raw-io"
"~@privileged"
"~@resources"
];
CapabilityBoundingSet = [
" " # Reset all capabilities to an empty set
];
RestrictAddressFamilies = [
" " # This is needed to clear the RestrictAddressFamilies existing definitions
"none" # Remove all addresses families
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
DevicePolicy = "closed";
ProtectKernelLogs = true;
SystemCallArchitectures = "native";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
EnvironmentFile = cfg.environmentFile;
ExecStart = lib.getExe cfg.package;
};
};
};
meta.maintainers = [ lib.maintainers.m0ustach3 ];
}

View File

@@ -0,0 +1,113 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.metabase;
inherit (lib) mkEnableOption mkIf mkOption;
inherit (lib) optional optionalAttrs types;
dataDir = "/var/lib/metabase";
in
{
options = {
services.metabase = {
enable = mkEnableOption "Metabase service";
package = lib.mkPackageOption pkgs "metabase" { };
listen = {
ip = mkOption {
type = types.str;
default = "0.0.0.0";
description = ''
IP address that Metabase should listen on.
'';
};
port = mkOption {
type = types.port;
default = 3000;
description = ''
Listen port for Metabase.
'';
};
};
ssl = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable SSL (https) support.
'';
};
port = mkOption {
type = types.port;
default = 8443;
description = ''
Listen port over SSL (https) for Metabase.
'';
};
keystore = mkOption {
type = types.nullOr types.path;
default = "${dataDir}/metabase.jks";
example = "/etc/secrets/keystore.jks";
description = ''
[Java KeyStore](https://www.digitalocean.com/community/tutorials/java-keytool-essentials-working-with-java-keystores) file containing the certificates.
'';
};
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for Metabase.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.metabase = {
description = "Metabase server";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = {
MB_PLUGINS_DIR = "${dataDir}/plugins";
MB_DB_FILE = "${dataDir}/metabase.db";
MB_JETTY_HOST = cfg.listen.ip;
MB_JETTY_PORT = toString cfg.listen.port;
}
// optionalAttrs (cfg.ssl.enable) {
MB_JETTY_SSL = true;
MB_JETTY_SSL_PORT = toString cfg.ssl.port;
MB_JETTY_SSL_KEYSTORE = cfg.ssl.keystore;
};
serviceConfig = {
DynamicUser = true;
StateDirectory = baseNameOf dataDir;
ExecStart = lib.getExe cfg.package;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listen.port ] ++ optional cfg.ssl.enable cfg.ssl.port;
};
};
}

View File

@@ -0,0 +1,148 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
getExe
mkIf
mkOption
mkEnableOption
types
;
cfg = config.services.mollysocket;
configuration = format.generate "mollysocket.conf" cfg.settings;
format = pkgs.formats.toml { };
package = pkgs.writeShellScriptBin "mollysocket" ''
MOLLY_CONF=${configuration} exec ${getExe pkgs.mollysocket} "$@"
'';
in
{
options.services.mollysocket = {
enable = mkEnableOption ''
[MollySocket](https://github.com/mollyim/mollysocket) for getting Signal
notifications via UnifiedPush
'';
settings = mkOption {
default = { };
description = ''
Configuration for MollySocket. Available options are listed
[here](https://github.com/mollyim/mollysocket#configuration).
'';
type = types.submodule {
freeformType = format.type;
options = {
host = mkOption {
default = "127.0.0.1";
description = "Listening address of the web server";
type = types.str;
};
port = mkOption {
default = 8020;
description = "Listening port of the web server";
type = types.port;
};
allowed_endpoints = mkOption {
default = [ "*" ];
description = "List of UnifiedPush servers";
example = [ "https://ntfy.sh" ];
type = with types; listOf str;
};
allowed_uuids = mkOption {
default = [ "*" ];
description = "UUIDs of Signal accounts that may use this server";
example = [ "abcdef-12345-tuxyz-67890" ];
type = with types; listOf str;
};
};
};
};
environmentFile = mkOption {
default = null;
description = ''
Environment file (see {manpage}`systemd.exec(5)` "EnvironmentFile="
section for the syntax) passed to the service. This option can be
used to safely include secrets in the configuration.
'';
example = "/run/secrets/mollysocket";
type = with types; nullOr path;
};
logLevel = mkOption {
default = "info";
description = "Set the {env}`RUST_LOG` environment variable";
example = "debug";
type = types.str;
};
};
config = mkIf cfg.enable {
environment.systemPackages = [
package
];
# see https://github.com/mollyim/mollysocket/blob/main/mollysocket.service
systemd.services.mollysocket = {
description = "MollySocket";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
environment.RUST_LOG = cfg.logLevel;
serviceConfig = {
EnvironmentFile = cfg.environmentFile;
ExecStart = "${getExe package} server";
KillSignal = "SIGINT";
Restart = "on-failure";
StateDirectory = "mollysocket";
TimeoutStopSec = 5;
WorkingDirectory = "/var/lib/mollysocket";
# hardening
DevicePolicy = "closed";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
"~@privileged"
];
UMask = "0077";
};
};
};
meta.maintainers = with lib.maintainers; [ dotlambda ];
}

View File

@@ -0,0 +1,252 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.moonraker;
pkg = cfg.package;
opt = options.services.moonraker;
format = pkgs.formats.ini {
# https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
listToValue =
l:
if builtins.length l == 1 then
lib.generators.mkValueStringDefault { } (lib.head l)
else
lib.concatMapStrings (s: "\n ${lib.generators.mkValueStringDefault { } s}") l;
mkKeyValue = lib.generators.mkKeyValueDefault { } ":";
};
unifiedConfigDir = cfg.stateDir + "/config";
in
{
options = {
services.moonraker = {
enable = lib.mkEnableOption "Moonraker, an API web server for Klipper";
package = lib.mkPackageOption pkgs "moonraker" {
nullable = true;
example = "moonraker.override { useGpiod = true; }";
};
klipperSocket = lib.mkOption {
type = lib.types.path;
default = config.services.klipper.apiSocket;
defaultText = lib.literalExpression "config.services.klipper.apiSocket";
description = "Path to Klipper's API socket.";
};
stateDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/moonraker";
description = "The directory containing the Moonraker databases.";
};
configDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Deprecated directory containing client-writable configuration files.
Clients will be able to edit files in this directory via the API. This directory must be writable.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "moonraker";
description = "User account under which Moonraker runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "moonraker";
description = "Group account under which Moonraker runs.";
};
address = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "The IP or host to listen on.";
};
port = lib.mkOption {
type = lib.types.port;
default = 7125;
description = "The port to listen on.";
};
settings = lib.mkOption {
type = format.type;
default = { };
example = {
authorization = {
trusted_clients = [ "10.0.0.0/24" ];
cors_domains = [
"https://app.fluidd.xyz"
"https://my.mainsail.xyz"
];
};
};
description = ''
Configuration for Moonraker. See the [documentation](https://moonraker.readthedocs.io/en/latest/configuration/)
for supported values.
'';
};
allowSystemControl = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to allow Moonraker to perform system-level operations.
Moonraker exposes APIs to perform system-level operations, such as
reboot, shutdown, and management of systemd units. See the
[documentation](https://moonraker.readthedocs.io/en/latest/web_api/#machine-commands)
for details on what clients are able to do.
'';
};
analysis.enable = lib.mkEnableOption "Runtime analysis with klipper-estimator";
};
};
config = lib.mkIf cfg.enable {
warnings =
[ ]
++ (lib.optional (lib.head (cfg.settings.update_manager.enable_system_updates or [ false ])) ''
Enabling system updates is not supported on NixOS and will lead to non-removable warnings in some clients.
'')
++ (lib.optional (cfg.configDir != null) ''
services.moonraker.configDir has been deprecated upstream and will be removed.
Action: ${
if cfg.configDir == unifiedConfigDir then
"Simply remove services.moonraker.configDir from your config."
else
"Move files from `${cfg.configDir}` to `${unifiedConfigDir}` then remove services.moonraker.configDir from your config."
}
'');
assertions = [
{
assertion = cfg.allowSystemControl -> config.security.polkit.enable;
message = "services.moonraker.allowSystemControl requires polkit to be enabled (security.polkit.enable).";
}
];
users.users = lib.optionalAttrs (cfg.user == "moonraker") {
moonraker = {
group = cfg.group;
uid = config.ids.uids.moonraker;
};
};
users.groups = lib.optionalAttrs (cfg.group == "moonraker") {
moonraker.gid = config.ids.gids.moonraker;
};
environment.etc."moonraker.cfg".source =
let
forcedConfig = {
server = {
host = cfg.address;
port = cfg.port;
klippy_uds_address = cfg.klipperSocket;
};
machine = {
validate_service = false;
};
}
// (lib.optionalAttrs (cfg.configDir != null) {
file_manager = {
config_path = cfg.configDir;
};
});
fullConfig = lib.recursiveUpdate cfg.settings forcedConfig;
in
format.generate "moonraker.cfg" fullConfig;
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
]
++ lib.optional (cfg.configDir != null) "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -"
++ lib.optionals cfg.analysis.enable [
"d '${cfg.stateDir}/tools/klipper_estimator' - ${cfg.user} ${cfg.group} - -"
"L+ '${cfg.stateDir}/tools/klipper_estimator/klipper_estimator_linux' - - - - ${lib.getExe pkgs.klipper-estimator}"
];
systemd.services.moonraker = {
description = "Moonraker, an API web server for Klipper";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ] ++ lib.optional config.services.klipper.enable "klipper.service";
# Moonraker really wants its own config to be writable...
script = ''
config_path=${
# Deprecated separate config dir
if cfg.configDir != null then
"${cfg.configDir}/moonraker-temp.cfg"
# Config in unified data path
else
"${unifiedConfigDir}/moonraker-temp.cfg"
}
mkdir -p $(dirname "$config_path")
cp /etc/moonraker.cfg "$config_path"
chmod u+w "$config_path"
exec ${pkg}/bin/moonraker -d ${cfg.stateDir} -c "$config_path"
'';
# Needs `ip` command
path = [ pkgs.iproute2 ];
serviceConfig = {
WorkingDirectory = cfg.stateDir;
PrivateTmp = true;
Group = cfg.group;
User = cfg.user;
};
};
services.moonraker.settings = {
# set this to false, otherwise we'll get a warning indicating that `/etc/klipper.cfg`
# is not located in the moonraker config directory.
file_manager.check_klipper_config_path = lib.mkIf (!config.services.klipper.mutableConfig) false;
# enable analysis with our own klipper-estimator, disable updating it
analysis = lib.mkIf (cfg.analysis.enable) {
platform = "linux";
enable_estimator_updates = false;
};
# suppress PolicyKit warnings if system control is disabled
machine.provider = lib.mkIf (!cfg.allowSystemControl) (lib.mkDefault "none");
};
security.polkit.extraConfig = lib.optionalString cfg.allowSystemControl ''
// nixos/moonraker: Allow Moonraker to perform system-level operations
//
// This was enabled via services.moonraker.allowSystemControl.
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.systemd1.manage-units" ||
action.id == "org.freedesktop.login1.power-off" ||
action.id == "org.freedesktop.login1.power-off-multiple-sessions" ||
action.id == "org.freedesktop.login1.reboot" ||
action.id == "org.freedesktop.login1.reboot-multiple-sessions" ||
action.id.startsWith("org.freedesktop.packagekit.")) &&
subject.user == "${cfg.user}") {
return polkit.Result.YES;
}
});
'';
};
meta.maintainers = with lib.maintainers; [
cab404
vtuan10
zhaofengli
];
}

View File

@@ -0,0 +1,250 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mqtt2influxdb;
filterNull = lib.filterAttrsRecursive (n: v: v != null);
configFile = (pkgs.formats.yaml { }).generate "mqtt2influxdb.config.yaml" (filterNull {
inherit (cfg) mqtt influxdb;
points = map filterNull cfg.points;
});
pointType = lib.types.submodule {
options = {
measurement = lib.mkOption {
type = lib.types.str;
description = "Name of the measurement";
};
topic = lib.mkOption {
type = lib.types.str;
description = "MQTT topic to subscribe to.";
};
fields = lib.mkOption {
type = lib.types.submodule {
options = {
value = lib.mkOption {
type = lib.types.str;
default = "$.payload";
description = "Value to be picked up";
};
type = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Type to be picked up";
};
};
};
description = "Field selector.";
};
tags = lib.mkOption {
type = with lib.types; attrsOf str;
default = { };
description = "Tags applied";
};
};
};
defaultPoints = [
{
measurement = "temperature";
topic = "node/+/thermometer/+/temperature";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
channel = "$.topic[3]";
};
}
{
measurement = "relative-humidity";
topic = "node/+/hygrometer/+/relative-humidity";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
channel = "$.topic[3]";
};
}
{
measurement = "illuminance";
topic = "node/+/lux-meter/0:0/illuminance";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
};
}
{
measurement = "pressure";
topic = "node/+/barometer/0:0/pressure";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
};
}
{
measurement = "co2";
topic = "node/+/co2-meter/-/concentration";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
};
}
{
measurement = "voltage";
topic = "node/+/battery/+/voltage";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
};
}
{
measurement = "button";
topic = "node/+/push-button/+/event-count";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
channel = "$.topic[3]";
};
}
{
measurement = "tvoc";
topic = "node/+/voc-lp-sensor/0:0/tvoc";
fields.value = "$.payload";
tags = {
id = "$.topic[1]";
};
}
];
in
{
options = {
services.mqtt2influxdb = {
enable = lib.mkEnableOption "BigClown MQTT to InfluxDB bridge";
package = lib.mkPackageOption pkgs [ "python3Packages" "mqtt2influxdb" ] { };
environmentFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
example = [ "/run/keys/mqtt2influxdb.env" ];
description = ''
File to load as environment file. Environment variables from this file
will be interpolated into the config file using envsubst with this
syntax: `$ENVIRONMENT` or `''${VARIABLE}`.
This is useful to avoid putting secrets into the nix store.
'';
};
mqtt = {
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Host where MQTT server is running.";
};
port = lib.mkOption {
type = lib.types.port;
default = 1883;
description = "MQTT server port.";
};
username = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Username used to connect to the MQTT server.";
};
password = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
MQTT password.
It is highly suggested to use here replacement through
environmentFiles as otherwise the password is put world readable to
the store.
'';
};
cafile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = "Certification Authority file for MQTT";
};
certfile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = "Certificate file for MQTT";
};
keyfile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = "Key file for MQTT";
};
};
influxdb = {
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Host where InfluxDB server is running.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8086;
description = "InfluxDB server port";
};
database = lib.mkOption {
type = lib.types.str;
description = "Name of the InfluxDB database.";
};
username = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Username for InfluxDB login.";
};
password = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
Password for InfluxDB login.
It is highly suggested to use here replacement through
environmentFiles as otherwise the password is put world readable to
the store.
'';
};
ssl = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use SSL to connect to the InfluxDB server.";
};
verify_ssl = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Verify SSL certificate when connecting to the InfluxDB server.";
};
};
points = lib.mkOption {
type = lib.types.listOf pointType;
default = defaultPoints;
description = "Points to bridge from MQTT to InfluxDB.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.bigclown-mqtt2influxdb =
let
envConfig = cfg.environmentFiles != [ ];
finalConfig = if envConfig then "$RUNTIME_DIRECTORY/mqtt2influxdb.config.yaml" else configFile;
in
{
description = "BigClown MQTT to InfluxDB bridge";
wantedBy = [ "multi-user.target" ];
wants = lib.mkIf config.services.mosquitto.enable [ "mosquitto.service" ];
preStart = ''
umask 077
${pkgs.envsubst}/bin/envsubst -i "${configFile}" -o "${finalConfig}"
'';
serviceConfig = {
EnvironmentFile = cfg.environmentFiles;
ExecStart = "${lib.getExe cfg.package} -dc ${finalConfig}";
RuntimeDirectory = "mqtt2influxdb";
};
};
};
}

View File

@@ -0,0 +1,94 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.n8n;
format = pkgs.formats.json { };
configFile = format.generate "n8n.json" cfg.settings;
in
{
options.services.n8n = {
enable = lib.mkEnableOption "n8n server";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the n8n web interface.";
};
settings = lib.mkOption {
type = format.type;
default = { };
description = ''
Configuration for n8n, see <https://docs.n8n.io/hosting/environment-variables/configuration-methods/>
for supported values.
'';
};
webhookUrl = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
WEBHOOK_URL for n8n, in case we're running behind a reverse proxy.
This cannot be set through configuration and must reside in an environment variable.
'';
};
};
config = lib.mkIf cfg.enable {
services.n8n.settings = {
# We use this to open the firewall, so we need to know about the default at eval time
port = lib.mkDefault 5678;
};
systemd.services.n8n = {
description = "N8N service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
# This folder must be writeable as the application is storing
# its data in it, so the StateDirectory is a good choice
N8N_USER_FOLDER = "/var/lib/n8n";
HOME = "/var/lib/n8n";
N8N_CONFIG_FILES = "${configFile}";
WEBHOOK_URL = "${cfg.webhookUrl}";
# Don't phone home
N8N_DIAGNOSTICS_ENABLED = "false";
N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
};
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.n8n}/bin/n8n";
Restart = "on-failure";
StateDirectory = "n8n";
# Basic Hardening
NoNewPrivileges = "yes";
PrivateTmp = "yes";
PrivateDevices = "yes";
DevicePolicy = "closed";
DynamicUser = "true";
ProtectSystem = "strict";
ProtectHome = "read-only";
ProtectControlGroups = "yes";
ProtectKernelModules = "yes";
ProtectKernelTunables = "yes";
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
RestrictNamespaces = "yes";
RestrictRealtime = "yes";
RestrictSUIDSGID = "yes";
MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable.
LockPersonality = "yes";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.settings.port ];
};
};
}

View File

@@ -0,0 +1,434 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.nitter;
configFile = pkgs.writeText "nitter.conf" ''
${lib.generators.toINI
{
# String values need to be quoted
mkKeyValue = lib.generators.mkKeyValueDefault {
mkValueString =
v:
if lib.isString v then
"\"" + (lib.escape [ "\"" ] (toString v)) + "\""
else
lib.generators.mkValueStringDefault { } v;
} " = ";
}
(
lib.recursiveUpdate {
Server = cfg.server;
Cache = cfg.cache;
Config = cfg.config // {
hmacKey = "@hmac@";
};
Preferences = cfg.preferences;
} cfg.settings
)
}
'';
# `hmac` is a secret used for cryptographic signing of video URLs.
# Generate it on first launch, then copy configuration and replace
# `@hmac@` with this value.
# We are not using sed as it would leak the value in the command line.
preStart = pkgs.writers.writePython3 "nitter-prestart" { } ''
import os
import secrets
state_dir = os.environ.get("STATE_DIRECTORY")
if not os.path.isfile(f"{state_dir}/hmac"):
# Generate hmac on first launch
hmac = secrets.token_hex(32)
with open(f"{state_dir}/hmac", "w") as f:
f.write(hmac)
else:
# Load previously generated hmac
with open(f"{state_dir}/hmac", "r") as f:
hmac = f.read()
configFile = "${configFile}"
with open(configFile, "r") as f_in:
with open(f"{state_dir}/nitter.conf", "w") as f_out:
f_out.write(f_in.read().replace("@hmac@", hmac))
'';
in
{
imports = [
# https://github.com/zedeus/nitter/pull/772
(lib.mkRemovedOptionModule [
"services"
"nitter"
"replaceInstagram"
] "Nitter no longer supports this option as Bibliogram has been discontinued.")
(lib.mkRenamedOptionModule
[ "services" "nitter" "guestAccounts" ]
[ "services" "nitter" "sessionsFile" ]
)
];
options = {
services.nitter = {
enable = lib.mkEnableOption "Nitter, an alternative Twitter front-end";
package = lib.mkPackageOption pkgs "nitter" { };
server = {
address = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
example = "127.0.0.1";
description = "The address to listen on.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
example = 8000;
description = "The port to listen on.";
};
https = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
};
httpMaxConnections = lib.mkOption {
type = lib.types.int;
default = 100;
description = "Maximum number of HTTP connections.";
};
staticDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.package}/share/nitter/public";
defaultText = lib.literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
description = "Path to the static files directory.";
};
title = lib.mkOption {
type = lib.types.str;
default = "nitter";
description = "Title of the instance.";
};
hostname = lib.mkOption {
type = lib.types.str;
default = "localhost";
example = "nitter.net";
description = "Hostname of the instance.";
};
};
cache = {
listMinutes = lib.mkOption {
type = lib.types.int;
default = 240;
description = "How long to cache list info (not the tweets, so keep it high).";
};
rssMinutes = lib.mkOption {
type = lib.types.int;
default = 10;
description = "How long to cache RSS queries.";
};
redisHost = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Redis host.";
};
redisPort = lib.mkOption {
type = lib.types.port;
default = 6379;
description = "Redis port.";
};
redisConnections = lib.mkOption {
type = lib.types.int;
default = 20;
description = "Redis connection pool size.";
};
redisMaxConnections = lib.mkOption {
type = lib.types.int;
default = 30;
description = ''
Maximum number of connections to Redis.
New connections are opened when none are available, but if the
pool size goes above this, they are closed when released, do not
worry about this unless you receive tons of requests per second.
'';
};
};
config = {
base64Media = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Use base64 encoding for proxied media URLs.";
};
enableRSS = lib.mkEnableOption "RSS feeds" // {
default = true;
};
enableDebug = lib.mkEnableOption "request logs and debug endpoints";
proxy = lib.mkOption {
type = lib.types.str;
default = "";
description = "URL to a HTTP/HTTPS proxy.";
};
proxyAuth = lib.mkOption {
type = lib.types.str;
default = "";
description = "Credentials for proxy.";
};
tokenCount = lib.mkOption {
type = lib.types.int;
default = 10;
description = ''
Minimum amount of usable tokens.
Tokens are used to authorize API requests, but they expire after
~1 hour, and have a limit of 187 requests. The limit gets reset
every 15 minutes, and the pool is filled up so there is always at
least tokenCount usable tokens. Only increase this if you receive
major bursts all the time.
'';
};
};
preferences = {
replaceTwitter = lib.mkOption {
type = lib.types.str;
default = "";
example = "nitter.net";
description = "Replace Twitter links with links to this instance (blank to disable).";
};
replaceYouTube = lib.mkOption {
type = lib.types.str;
default = "";
example = "piped.kavin.rocks";
description = "Replace YouTube links with links to this instance (blank to disable).";
};
replaceReddit = lib.mkOption {
type = lib.types.str;
default = "";
example = "teddit.net";
description = "Replace Reddit links with links to this instance (blank to disable).";
};
mp4Playback = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable MP4 video playback.";
};
hlsPlayback = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable HLS video streaming (requires JavaScript).";
};
proxyVideos = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Proxy video streaming through the server (might be slow).";
};
muteVideos = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Mute videos by default.";
};
autoplayGifs = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Autoplay GIFs.";
};
theme = lib.mkOption {
type = lib.types.str;
default = "Nitter";
description = "Instance theme.";
};
infiniteScroll = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Infinite scrolling (requires JavaScript, experimental!).";
};
stickyProfile = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Make profile sidebar stick to top.";
};
bidiSupport = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Support bidirectional text (makes clicking on tweets harder).";
};
hideTweetStats = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Hide tweet stats (replies, retweets, likes).";
};
hideBanner = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Hide profile banner.";
};
hidePins = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Hide pinned tweets.";
};
hideReplies = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Hide tweet replies.";
};
squareAvatars = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Square profile pictures.";
};
};
settings = lib.mkOption {
type = lib.types.attrs;
default = { };
description = ''
Add settings here to override NixOS module generated settings.
Check the official repository for the available settings:
<https://github.com/zedeus/nitter/blob/master/nitter.example.conf>
'';
};
sessionsFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/nitter/sessions.jsonl";
description = ''
Path to the session tokens file.
This file contains a list of session tokens that can be used to
access the instance without logging in. The file is in JSONL format,
where each line is a JSON object with the following fields:
{"oauth_token":"some_token","oauth_token_secret":"some_secret_key"}
See <https://github.com/zedeus/nitter/wiki/Creating-session-tokens>
for more information on session tokens and how to generate them.
'';
};
redisCreateLocally = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Configure local Redis server for Nitter.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for Nitter web interface.";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
!cfg.redisCreateLocally || (cfg.cache.redisHost == "localhost" && cfg.cache.redisPort == 6379);
message = "When services.nitter.redisCreateLocally is enabled, you need to use localhost:6379 as a cache server.";
}
];
systemd.services.nitter = {
description = "Nitter (An alternative Twitter front-end)";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
DynamicUser = true;
LoadCredential = "sessionsFile:${cfg.sessionsFile}";
StateDirectory = "nitter";
Environment = [
"NITTER_CONF_FILE=/var/lib/nitter/nitter.conf"
"NITTER_SESSIONS_FILE=%d/sessionsFile"
];
# Some parts of Nitter expect `public` folder in working directory,
# see https://github.com/zedeus/nitter/issues/414
WorkingDirectory = "${cfg.package}/share/nitter";
ExecStart = "${cfg.package}/bin/nitter";
ExecStartPre = "${preStart}";
AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
Restart = "on-failure";
RestartSec = "5s";
# Hardening
CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
DeviceAllow = [ "" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateDevices = true;
# A private user cannot have process capabilities on the host's user
# namespace and thus CAP_NET_BIND_SERVICE has no effect.
PrivateUsers = (cfg.server.port >= 1024);
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
};
};
services.redis.servers.nitter = lib.mkIf (cfg.redisCreateLocally) {
enable = true;
port = cfg.cache.redisPort;
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.server.port ];
};
};
}

View File

@@ -0,0 +1,104 @@
{ config, lib, ... }:
let
cfg = config.nix.gc;
in
{
###### interface
options = {
nix.gc = {
automatic = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Automatically run the garbage collector at a specific time.";
};
dates = lib.mkOption {
type = with lib.types; either singleLineStr (listOf str);
apply = lib.toList;
default = [ "03:15" ];
example = "weekly";
description = ''
How often or when garbage collection is performed. For most desktop and server systems
a sufficient garbage collection is once a week.
This value must be a calendar event in the format specified by
{manpage}`systemd.time(7)`.
'';
};
randomizedDelaySec = lib.mkOption {
default = "0";
type = lib.types.singleLineStr;
example = "45min";
description = ''
Add a randomized delay before each garbage collection.
The delay will be chosen between zero and this value.
This value must be a time span in the format specified by
{manpage}`systemd.time(7)`
'';
};
persistent = lib.mkOption {
default = true;
type = lib.types.bool;
example = false;
description = ''
Takes a boolean argument. If true, the time when the service
unit was last triggered is stored on disk. When the timer is
activated, the service unit is triggered immediately if it
would have been triggered at least once during the time when
the timer was inactive. Such triggering is nonetheless
subject to the delay imposed by RandomizedDelaySec=. This is
useful to catch up on missed runs of the service when the
system was powered down.
'';
};
options = lib.mkOption {
default = "";
example = "--max-freed $((64 * 1024**3))";
type = lib.types.singleLineStr;
description = ''
Options given to [`nix-collect-garbage`](https://nixos.org/manual/nix/stable/command-ref/nix-collect-garbage) when the garbage collector is run automatically.
'';
};
};
};
###### implementation
config = {
assertions = [
{
assertion = cfg.automatic -> config.nix.enable;
message = ''nix.gc.automatic requires nix.enable'';
}
];
systemd.services.nix-gc = lib.mkIf config.nix.enable {
description = "Nix Garbage Collector";
script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
serviceConfig.Type = "oneshot";
startAt = lib.optionals cfg.automatic cfg.dates;
# do not start and delay when switching
restartIfChanged = false;
};
systemd.timers.nix-gc = lib.mkIf cfg.automatic {
timerConfig = {
RandomizedDelaySec = cfg.randomizedDelaySec;
Persistent = cfg.persistent;
};
};
};
}

View File

@@ -0,0 +1,84 @@
{ config, lib, ... }:
let
cfg = config.nix.optimise;
in
{
options = {
nix.optimise = {
automatic = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Automatically run the nix store optimiser at a specific time.";
};
dates = lib.mkOption {
default = [ "03:45" ];
apply = lib.toList;
type = with lib.types; either singleLineStr (listOf str);
description = ''
Specification (in the format described by
{manpage}`systemd.time(7)`) of the time at
which the optimiser will run.
'';
};
randomizedDelaySec = lib.mkOption {
default = "1800";
type = lib.types.singleLineStr;
example = "45min";
description = ''
Add a randomized delay before the optimizer will run.
The delay will be chosen between zero and this value.
This value must be a time span in the format specified by
{manpage}`systemd.time(7)`
'';
};
persistent = lib.mkOption {
default = true;
type = lib.types.bool;
example = false;
description = ''
Takes a boolean argument. If true, the time when the service
unit was last triggered is stored on disk. When the timer is
activated, the service unit is triggered immediately if it
would have been triggered at least once during the time when
the timer was inactive. Such triggering is nonetheless
subject to the delay imposed by RandomizedDelaySec=. This is
useful to catch up on missed runs of the service when the
system was powered down.
'';
};
};
};
config = {
assertions = [
{
assertion = cfg.automatic -> config.nix.enable;
message = ''nix.optimise.automatic requires nix.enable'';
}
];
systemd = lib.mkIf config.nix.enable {
services.nix-optimise = {
description = "Nix Store Optimiser";
# No point this if the nix daemon (and thus the nix store) is outside
unitConfig.ConditionPathIsReadWrite = "/nix/var/nix/daemon-socket";
serviceConfig.ExecStart = "${config.nix.package}/bin/nix-store --optimise";
startAt = lib.optionals cfg.automatic cfg.dates;
# do not start and delay when switching
restartIfChanged = false;
};
timers.nix-optimise = lib.mkIf cfg.automatic {
timerConfig = {
RandomizedDelaySec = cfg.randomizedDelaySec;
Persistent = cfg.persistent;
};
};
};
};
}

View File

@@ -0,0 +1,86 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.nix.sshServe;
command =
if cfg.protocol == "ssh" then
"nix-store --serve ${lib.optionalString cfg.write "--write"}"
else
"nix-daemon --stdio";
in
{
options = {
nix.sshServe = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable serving the Nix store as a remote store via SSH.";
};
write = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable writing to the Nix store as a remote store via SSH. Note: by default, the sshServe user is named nix-ssh and is not a trusted-user. nix-ssh should be added to the {option}`nix.sshServe.trusted` option in most use cases, such as allowing remote building of derivations to anonymous people based on ssh key";
};
trusted = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add nix-ssh to the nix.settings.trusted-users";
};
keys = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "ssh-dss AAAAB3NzaC1k... alice@example.org" ];
description = "A list of SSH public keys allowed to access the binary cache via SSH.";
};
protocol = lib.mkOption {
type = lib.types.enum [
"ssh"
"ssh-ng"
];
default = "ssh";
description = "The specific Nix-over-SSH protocol to use.";
};
};
};
config = lib.mkIf cfg.enable {
users.users.nix-ssh = {
description = "Nix SSH store user";
isSystemUser = true;
group = "nix-ssh";
shell = pkgs.bashInteractive;
};
users.groups.nix-ssh = { };
nix.settings.trusted-users = lib.mkIf cfg.trusted [ "nix-ssh" ];
services.openssh.enable = true;
services.openssh.extraConfig = ''
Match User nix-ssh
AllowAgentForwarding no
AllowTcpForwarding no
PermitTTY no
PermitTunnel no
X11Forwarding no
ForceCommand ${config.nix.package.out}/bin/${command}
Match All
'';
users.users.nix-ssh.openssh.authorizedKeys.keys = cfg.keys;
};
}

View File

@@ -0,0 +1,34 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.novacomd;
in
{
options = {
services.novacomd = {
enable = lib.mkEnableOption "Novacom service for connecting to WebOS devices";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.webos.novacom ];
systemd.services.novacomd = {
description = "Novacom WebOS daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.webos.novacomd}/sbin/novacomd";
};
};
};
meta.maintainers = with lib.maintainers; [ dtzWill ];
}

View File

@@ -0,0 +1,139 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ntfy-sh;
settingsFormat = pkgs.formats.yaml { };
in
{
options.services.ntfy-sh = {
enable = lib.mkEnableOption "[ntfy-sh](https://ntfy.sh), a push notification service";
package = lib.mkPackageOption pkgs "ntfy-sh" { };
user = lib.mkOption {
default = "ntfy-sh";
type = lib.types.str;
description = "User the ntfy-sh server runs under.";
};
group = lib.mkOption {
default = "ntfy-sh";
type = lib.types.str;
description = "Primary group of ntfy-sh user.";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
base-url = lib.mkOption {
type = lib.types.str;
example = "https://ntfy.example";
description = ''
Public facing base URL of the service
This setting is required for any of the following features:
- attachments (to return a download URL)
- e-mail sending (for the topic URL in the email footer)
- iOS push notifications for self-hosted servers
(to calculate the Firebase poll_request topic)
- Matrix Push Gateway (to validate that the pushkey is correct)
'';
};
};
};
default = { };
example = lib.literalExpression ''
{
listen-http = ":8080";
}
'';
description = ''
Configuration for ntfy.sh, supported values are [here](https://ntfy.sh/docs/config/#config-options).
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/ntfy";
description = ''
Path to a file containing extra ntfy environment variables in the systemd `EnvironmentFile`
format. Refer to the [documentation](https://docs.ntfy.sh/config/) for config options.
This can be used to pass secrets such as creating declarative users or token without putting them in the Nix store.
'';
};
};
config =
let
configuration = settingsFormat.generate "server.yml" cfg.settings;
in
lib.mkIf cfg.enable {
# to configure access control via the cli
environment = {
etc."ntfy/server.yml".source = configuration;
systemPackages = [ cfg.package ];
};
services.ntfy-sh.settings = {
auth-file = lib.mkDefault "/var/lib/ntfy-sh/user.db";
listen-http = lib.mkDefault "127.0.0.1:2586";
attachment-cache-dir = lib.mkDefault "/var/lib/ntfy-sh/attachments";
cache-file = lib.mkDefault "/var/lib/ntfy-sh/cache-file.db";
};
systemd.services.ntfy-sh = {
description = "Push notifications server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/ntfy serve -c ${configuration}";
User = cfg.user;
StateDirectory = "ntfy-sh";
DynamicUser = true;
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
PrivateTmp = true;
NoNewPrivileges = true;
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
ProtectSystem = "full";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
PrivateDevices = true;
RestrictSUIDSGID = true;
RestrictNamespaces = true;
RestrictRealtime = true;
MemoryDenyWriteExecute = true;
# Upstream Recommendation
LimitNOFILE = 20500;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
};
};
users.groups = lib.optionalAttrs (cfg.group == "ntfy-sh") {
ntfy-sh = { };
};
users.users = lib.optionalAttrs (cfg.user == "ntfy-sh") {
ntfy-sh = {
isSystemUser = true;
group = cfg.group;
};
};
};
}

View File

@@ -0,0 +1,145 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.nzbget;
stateDir = "/var/lib/nzbget";
configFile = "${stateDir}/nzbget.conf";
configOpts = lib.concatStringsSep " " (
lib.mapAttrsToList (name: value: "-o ${name}=${lib.escapeShellArg (toStr value)}") cfg.settings
);
toStr =
v:
if v == true then
"yes"
else if v == false then
"no"
else if lib.isInt v then
toString v
else
v;
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"misc"
"nzbget"
"configFile"
] "The configuration of nzbget is now managed by users through the web interface.")
(lib.mkRemovedOptionModule [
"services"
"misc"
"nzbget"
"dataDir"
] "The data directory for nzbget is now /var/lib/nzbget.")
(lib.mkRemovedOptionModule [ "services" "misc" "nzbget" "openFirewall" ]
"The port used by nzbget is managed through the web interface so you should adjust your firewall rules accordingly."
)
];
# interface
options = {
services.nzbget = {
enable = lib.mkEnableOption "NZBGet, for downloading files from news servers";
package = lib.mkPackageOption pkgs "nzbget" { };
user = lib.mkOption {
type = lib.types.str;
default = "nzbget";
description = "User account under which NZBGet runs";
};
group = lib.mkOption {
type = lib.types.str;
default = "nzbget";
description = "Group under which NZBGet runs";
};
settings = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
bool
int
str
]);
default = { };
description = ''
NZBGet configuration, passed via command line using switch -o. Refer to
<https://github.com/nzbgetcom/nzbget/blob/develop/nzbget.conf>
for details on supported values.
'';
example = {
MainDir = "/data";
};
};
};
};
# implementation
config = lib.mkIf cfg.enable {
services.nzbget.settings = {
# allows nzbget to run as a "simple" service
OutputMode = "loggable";
# use journald for logging
WriteLog = "none";
ErrorTarget = "screen";
WarningTarget = "screen";
InfoTarget = "screen";
DetailTarget = "screen";
# required paths
ConfigTemplate = "${cfg.package}/share/nzbget/nzbget.conf";
WebDir = "${cfg.package}/share/nzbget/webui";
# nixos handles package updates
UpdateCheck = "none";
};
systemd.services.nzbget = {
description = "NZBGet Daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
unrar
p7zip
];
preStart = ''
if [ ! -f ${configFile} ]; then
${pkgs.coreutils}/bin/install -m 0700 ${cfg.package}/share/nzbget/nzbget.conf ${configFile}
fi
'';
serviceConfig = {
StateDirectory = "nzbget";
StateDirectoryMode = "0750";
User = cfg.user;
Group = cfg.group;
UMask = "0002";
Restart = "on-failure";
ExecStart = "${cfg.package}/bin/nzbget --server --configfile ${stateDir}/nzbget.conf ${configOpts}";
ExecStop = "${cfg.package}/bin/nzbget --quit";
};
};
users.users = lib.mkIf (cfg.user == "nzbget") {
nzbget = {
home = stateDir;
group = cfg.group;
uid = config.ids.uids.nzbget;
};
};
users.groups = lib.mkIf (cfg.group == "nzbget") {
nzbget = {
gid = config.ids.gids.nzbget;
};
};
};
}

View File

@@ -0,0 +1,74 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.nzbhydra2;
in
{
options = {
services.nzbhydra2 = {
enable = lib.mkEnableOption "NZBHydra2, Usenet meta search";
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/nzbhydra2";
description = "The directory where NZBHydra2 stores its data files.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for the NZBHydra2 web interface.";
};
package = lib.mkPackageOption pkgs "nzbhydra2" { };
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 nzbhydra2 nzbhydra2 - -" ];
systemd.services.nzbhydra2 = {
description = "NZBHydra2";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = "nzbhydra2";
Group = "nzbhydra2";
ExecStart = "${cfg.package}/bin/nzbhydra2 --nobrowser --datafolder '${cfg.dataDir}'";
Restart = "on-failure";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
DevicePolicy = "closed";
ProtectSystem = "strict";
ReadWritePaths = cfg.dataDir;
ProtectHome = "read-only";
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
};
};
networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ 5076 ]; };
users.users.nzbhydra2 = {
group = "nzbhydra2";
isSystemUser = true;
};
users.groups.nzbhydra2 = { };
};
}

Some files were not shown because too many files have changed in this diff Show More