uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -0,0 +1,179 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.airsonic;
opt = options.services.airsonic;
in {
options = {
services.airsonic = {
enable = mkEnableOption "Airsonic, the Free and Open Source media streaming server (fork of Subsonic and Libresonic)";
user = mkOption {
type = types.str;
default = "airsonic";
description = "User account under which airsonic runs.";
};
home = mkOption {
type = types.path;
default = "/var/lib/airsonic";
description = ''
The directory where Airsonic will create files.
Make sure it is writable.
'';
};
virtualHost = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
'';
};
listenAddress = mkOption {
type = 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 = mkOption {
type = types.int;
default = 4040;
description = ''
The port on which Airsonic will listen for
incoming HTTP traffic. Set to 0 to disable.
'';
};
contextPath = mkOption {
type = types.path;
default = "/";
description = ''
The context path, i.e., the last part of the Airsonic
URL. Typically '/' or '/airsonic'. Default '/'
'';
};
maxMemory = mkOption {
type = types.int;
default = 100;
description = ''
The memory limit (max Java heap size) in megabytes.
Default: 100
'';
};
transcoders = mkOption {
type = types.listOf types.path;
default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
defaultText = 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 = mkOption {
type = types.package;
default = pkgs.jre8;
defaultText = literalExpression "pkgs.jre8";
description = ''
JRE package to use.
Airsonic only supports Java 8, airsonic-advanced requires at least
Java 11.
'';
};
war = mkOption {
type = types.path;
default = "${pkgs.airsonic}/webapps/airsonic.war";
defaultText = literalExpression ''"''${pkgs.airsonic}/webapps/airsonic.war"'';
description = "Airsonic war file to use.";
};
jvmOptions = mkOption {
description = ''
Extra command line options for the JVM running AirSonic.
Useful for sending jukebox output to non-default alsa
devices.
'';
default = [
];
type = types.listOf 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 = 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} \
-Dairsonic.contextPath=${cfg.contextPath} \
-Djava.awt.headless=true \
${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 = 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,107 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ananicy;
configFile = pkgs.writeText "ananicy.conf" (generators.toKeyValue { } cfg.settings);
extraRules = pkgs.writeText "extraRules" cfg.extraRules;
servicename = if ((lib.getName cfg.package) == (lib.getName pkgs.ananicy-cpp)) then "ananicy-cpp" else "ananicy";
in
{
options = {
services.ananicy = {
enable = mkEnableOption "Ananicy, an auto nice daemon";
package = mkOption {
type = types.package;
default = pkgs.ananicy;
defaultText = literalExpression "pkgs.ananicy";
example = literalExpression "pkgs.ananicy-cpp";
description = ''
Which ananicy package to use.
'';
};
settings = mkOption {
type = with types; attrsOf (oneOf [ int bool str ]);
default = { };
example = {
apply_nice = false;
};
description = ''
See <link xlink:href="https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf"/>
'';
};
extraRules = mkOption {
type = types.str;
default = "";
description = ''
Extra rules in json format on separate lines. See:
<link xlink:href="https://github.com/Nefelim4ag/Ananicy#configuration"/>
<link xlink:href="https://gitlab.com/ananicy-cpp/ananicy-cpp/#global-configuration"/>
'';
example = literalExpression ''
'''
{ "name": "eog", "type": "Image-View" }
{ "name": "fdupes", "type": "BG_CPUIO" }
'''
'';
};
};
};
config = mkIf cfg.enable {
environment = {
systemPackages = [ cfg.package ];
etc."ananicy.d".source = pkgs.runCommandLocal "ananicyfiles" { } ''
mkdir -p $out
# ananicy-cpp does not include rules or settings on purpose
cp -r ${pkgs.ananicy}/etc/ananicy.d/* $out
rm $out/ananicy.conf
cp ${configFile} $out/ananicy.conf
${optionalString (cfg.extraRules != "") "cp ${extraRules} $out/nixRules.rules"}
'';
};
# ananicy and ananicy-cpp have different default settings
services.ananicy.settings =
let
mkOD = 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 ((lib.getName cfg.package) == (lib.getName pkgs.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 = mkOD config.systemd.enableUnifiedCgroupHierarchy;
} else {
# https://github.com/Nefelim4ag/Ananicy/blob/master/ananicy.d/ananicy.conf
check_disks_schedulers = mkOD true;
check_freq = mkOD 5;
});
systemd = {
# https://gitlab.com/ananicy-cpp/ananicy-cpp/#cgroups applies to both ananicy and -cpp
enableUnifiedCgroupHierarchy = mkDefault false;
packages = [ cfg.package ];
services."${servicename}" = {
wantedBy = [ "default.target" ];
};
};
};
meta = {
maintainers = with maintainers; [ artturin ];
};
}

View file

@ -0,0 +1,79 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ankisyncd;
name = "ankisyncd";
stateDir = "/var/lib/${name}";
authDbPath = "${stateDir}/auth.db";
sessionDbPath = "${stateDir}/session.db";
configFile = pkgs.writeText "ankisyncd.conf" (lib.generators.toINI {} {
sync_app = {
host = cfg.host;
port = cfg.port;
data_root = stateDir;
auth_db_path = authDbPath;
session_db_path = sessionDbPath;
base_url = "/sync/";
base_media_url = "/msync/";
};
});
in
{
options.services.ankisyncd = {
enable = mkEnableOption "ankisyncd";
package = mkOption {
type = types.package;
default = pkgs.ankisyncd;
defaultText = literalExpression "pkgs.ankisyncd";
description = "The package to use for the ankisyncd command.";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "ankisyncd host";
};
port = mkOption {
type = types.int;
default = 27701;
description = "ankisyncd port";
};
openFirewall = mkOption {
default = false;
type = types.bool;
description = "Whether to open the firewall for the specified port.";
};
};
config = mkIf cfg.enable {
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
environment.etc."ankisyncd/ankisyncd.conf".source = configFile;
systemd.services.ankisyncd = {
description = "ankisyncd - Anki sync server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ cfg.package ];
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = name;
ExecStart = "${cfg.package}/bin/ankisyncd";
Restart = "always";
};
};
};
}

View file

@ -0,0 +1,151 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.apache-kafka;
serverProperties =
if cfg.serverProperties != null then
cfg.serverProperties
else
''
# Generated by nixos
broker.id=${toString cfg.brokerId}
port=${toString cfg.port}
host.name=${cfg.hostname}
log.dirs=${concatStringsSep "," cfg.logDirs}
zookeeper.connect=${cfg.zookeeper}
${toString cfg.extraProperties}
'';
serverConfig = pkgs.writeText "server.properties" serverProperties;
logConfig = pkgs.writeText "log4j.properties" cfg.log4jProperties;
in {
options.services.apache-kafka = {
enable = mkOption {
description = "Whether to enable Apache Kafka.";
default = false;
type = types.bool;
};
brokerId = mkOption {
description = "Broker ID.";
default = -1;
type = types.int;
};
port = mkOption {
description = "Port number the broker should listen on.";
default = 9092;
type = types.int;
};
hostname = mkOption {
description = "Hostname the broker should bind to.";
default = "localhost";
type = types.str;
};
logDirs = mkOption {
description = "Log file directories";
default = [ "/tmp/kafka-logs" ];
type = types.listOf types.path;
};
zookeeper = mkOption {
description = "Zookeeper connection string";
default = "localhost:2181";
type = types.str;
};
extraProperties = mkOption {
description = "Extra properties for server.properties.";
type = types.nullOr types.lines;
default = null;
};
serverProperties = mkOption {
description = ''
Complete server.properties content. Other server.properties config
options will be ignored if this option is used.
'';
type = types.nullOr types.lines;
default = null;
};
log4jProperties = 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 = types.lines;
};
jvmOptions = mkOption {
description = "Extra command line options for the JVM running Kafka.";
default = [];
type = types.listOf types.str;
example = [
"-Djava.net.preferIPv4Stack=true"
"-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.local.only=true"
];
};
package = mkOption {
description = "The kafka package to use";
default = pkgs.apacheKafka;
defaultText = literalExpression "pkgs.apacheKafka";
type = types.package;
};
jre = mkOption {
description = "The JRE with which to run Kafka";
default = cfg.package.passthru.jre;
defaultText = literalExpression "pkgs.apacheKafka.passthru.jre";
type = types.package;
};
};
config = mkIf cfg.enable {
environment.systemPackages = [cfg.package];
users.users.apache-kafka = {
isSystemUser = true;
group = "apache-kafka";
description = "Apache Kafka daemon user";
home = head cfg.logDirs;
};
users.groups.apache-kafka = {};
systemd.tmpfiles.rules = map (logDir: "d '${logDir}' 0700 apache-kafka - - -") cfg.logDirs;
systemd.services.apache-kafka = {
description = "Apache Kafka Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = ''
${cfg.jre}/bin/java \
-cp "${cfg.package}/libs/*" \
-Dlog4j.configuration=file:${logConfig} \
${toString cfg.jvmOptions} \
kafka.Kafka \
${serverConfig}
'';
User = "apache-kafka";
SuccessExitStatus = "0 143";
};
};
};
}

View file

@ -0,0 +1,100 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.autofs;
autoMaster = pkgs.writeText "auto.master" cfg.autoMaster;
in
{
###### interface
options = {
services.autofs = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Mount filesystems on demand. Unmount them automatically.
You may also be interested in afuse.
'';
};
autoMaster = mkOption {
type = types.str;
example = 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 <literal>/etc/auto.master</literal> file. See <command>auto.master(5)</command> and <command>autofs(5)</command>.
'';
};
timeout = mkOption {
type = types.int;
default = 600;
description = "Set the global minimum timeout, in seconds, until directories are unmounted";
};
debug = mkOption {
type = types.bool;
default = false;
description = ''
Pass -d and -7 to automount and write log to the system journal.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
boot.kernelModules = [ "autofs4" ];
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 ${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,359 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.autorandr;
hookType = types.lines;
matrixOf = n: m: elemType:
mkOptionType rec {
name = "matrixOf";
description =
"${toString n}×${toString m} matrix of ${elemType.description}s";
check = xss:
let listOfSize = l: xs: isList xs && length xs == l;
in listOfSize n xss
&& all (xs: listOfSize m xs && all elemType.check xs) xss;
merge = mergeOneOption;
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
getSubModules = elemType.getSubModules;
substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
functor = (defaultFunctor name) // { wrapped = elemType; };
};
profileModule = types.submodule {
options = {
fingerprint = mkOption {
type = types.attrsOf types.str;
description = ''
Output name to EDID mapping.
Use <code>autorandr --fingerprint</code> to get current setup values.
'';
default = { };
};
config = mkOption {
type = types.attrsOf configModule;
description = "Per output profile configuration.";
default = { };
};
hooks = mkOption {
type = hooksModule;
description = "Profile hook scripts.";
default = { };
};
};
};
configModule = types.submodule {
options = {
enable = mkOption {
type = types.bool;
description = "Whether to enable the output.";
default = true;
};
crtc = mkOption {
type = types.nullOr types.ints.unsigned;
description = "Output video display controller.";
default = null;
example = 0;
};
primary = mkOption {
type = types.bool;
description = "Whether output should be marked as primary";
default = false;
};
position = mkOption {
type = types.str;
description = "Output position";
default = "";
example = "5760x0";
};
mode = mkOption {
type = types.str;
description = "Output resolution.";
default = "";
example = "3840x2160";
};
rate = mkOption {
type = types.str;
description = "Output framerate.";
default = "";
example = "60.00";
};
gamma = mkOption {
type = types.str;
description = "Output gamma configuration.";
default = "";
example = "1.0:0.909:0.833";
};
rotate = mkOption {
type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
description = "Output rotate configuration.";
default = null;
example = "left";
};
transform = mkOption {
type = types.nullOr (matrixOf 3 3 types.float);
default = null;
example = literalExpression ''
[
[ 0.6 0.0 0.0 ]
[ 0.0 0.6 0.0 ]
[ 0.0 0.0 1.0 ]
]
'';
description = ''
Refer to
<citerefentry>
<refentrytitle>xrandr</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
for the documentation of the transform matrix.
'';
};
dpi = mkOption {
type = types.nullOr types.ints.positive;
description = "Output DPI configuration.";
default = null;
example = 96;
};
scale = mkOption {
type = types.nullOr (types.submodule {
options = {
method = mkOption {
type = types.enum [ "factor" "pixel" ];
description = "Output scaling method.";
default = "factor";
example = "pixel";
};
x = mkOption {
type = types.either types.float types.ints.positive;
description = "Horizontal scaling factor/pixels.";
};
y = mkOption {
type = types.either types.float types.ints.positive;
description = "Vertical scaling factor/pixels.";
};
};
});
description = ''
Output scale configuration.
</para><para>
Either configure by pixels or a scaling factor. When using pixel method the
<citerefentry>
<refentrytitle>xrandr</refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
option
<parameter class="command">--scale-from</parameter>
will be used; when using factor method the option
<parameter class="command">--scale</parameter>
will be used.
</para><para>
This option is a shortcut version of the transform option and they are mutually
exclusive.
'';
default = null;
example = literalExpression ''
{
x = 1.25;
y = 1.25;
}
'';
};
};
};
hooksModule = types.submodule {
options = {
postswitch = mkOption {
type = types.attrsOf hookType;
description = "Postswitch hook executed after mode switch.";
default = { };
};
preswitch = mkOption {
type = types.attrsOf hookType;
description = "Preswitch hook executed before mode switch.";
default = { };
};
predetect = mkOption {
type = types.attrsOf hookType;
description = ''
Predetect hook executed before autorandr attempts to run xrandr.
'';
default = { };
};
};
};
hookToFile = folder: name: hook:
nameValuePair "xdg/autorandr/${folder}/${name}" {
source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
};
profileToFiles = name: profile:
with profile;
mkMerge ([
{
"xdg/autorandr/${name}/setup".text = concatStringsSep "\n"
(mapAttrsToList fingerprintToString fingerprint);
"xdg/autorandr/${name}/config".text =
concatStringsSep "\n" (mapAttrsToList configToString profile.config);
}
(mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
(mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
(mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
]);
fingerprintToString = name: edid: "${name} ${edid}";
configToString = name: config:
if config.enable then
concatStringsSep "\n" ([ "output ${name}" ]
++ optional (config.position != "") "pos ${config.position}"
++ optional (config.crtc != null) "crtc ${toString config.crtc}"
++ optional config.primary "primary"
++ optional (config.dpi != null) "dpi ${toString config.dpi}"
++ optional (config.gamma != "") "gamma ${config.gamma}"
++ optional (config.mode != "") "mode ${config.mode}"
++ optional (config.rate != "") "rate ${config.rate}"
++ optional (config.rotate != null) "rotate ${config.rotate}"
++ optional (config.transform != null) ("transform "
+ concatMapStringsSep "," toString (flatten config.transform))
++ 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 = mkEnableOption "handling of hotplug and sleep events by autorandr";
defaultTarget = mkOption {
default = "default";
type = 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.
'';
};
hooks = mkOption {
type = hooksModule;
description = "Global hook scripts";
default = { };
example = ''
{
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 = mkOption {
type = types.attrsOf profileModule;
description = "Autorandr profiles specification.";
default = { };
example = 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 = mkIf cfg.enable {
services.udev.packages = [ pkgs.autorandr ];
environment = {
systemPackages = [ pkgs.autorandr ];
etc = mkMerge ([
(mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
(mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
(mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
(mkMerge (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}";
Type = "oneshot";
RemainAfterExit = false;
KillMode = "process";
};
};
};
meta.maintainers = with maintainers; [ alexnortung ];
}

View file

@ -0,0 +1,77 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.bazarr;
in
{
options = {
services.bazarr = {
enable = mkEnableOption "bazarr, a subtitle manager for Sonarr and Radarr";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the bazarr web interface.";
};
listenPort = mkOption {
type = types.port;
default = 6767;
description = "Port on which the bazarr web interface should listen";
};
user = mkOption {
type = types.str;
default = "bazarr";
description = "User account under which bazarr runs.";
};
group = mkOption {
type = types.str;
default = "bazarr";
description = "Group under which bazarr runs.";
};
};
};
config = mkIf cfg.enable {
systemd.services.bazarr = {
description = "bazarr";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = rec {
Type = "simple";
User = cfg.user;
Group = cfg.group;
StateDirectory = "bazarr";
SyslogIdentifier = "bazarr";
ExecStart = pkgs.writeShellScript "start-bazarr" ''
${pkgs.bazarr}/bin/bazarr \
--config '/var/lib/${StateDirectory}' \
--port ${toString cfg.listenPort} \
--no-update True
'';
Restart = "on-failure";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listenPort ];
};
users.users = mkIf (cfg.user == "bazarr") {
bazarr = {
isSystemUser = true;
group = cfg.group;
home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
};
};
users.groups = mkIf (cfg.group == "bazarr") {
bazarr = {};
};
};
}

View file

@ -0,0 +1,63 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.beanstalkd;
pkg = pkgs.beanstalkd;
in
{
# interface
options = {
services.beanstalkd = {
enable = mkEnableOption "the Beanstalk work queue";
listen = {
port = mkOption {
type = types.int;
description = "TCP port that will be used to accept client connections.";
default = 11300;
};
address = mkOption {
type = types.str;
description = "IP address to listen on.";
default = "127.0.0.1";
example = "0.0.0.0";
};
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Whether to open ports in the firewall for the server.";
};
};
};
# implementation
config = mkIf cfg.enable {
networking.firewall = 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, ... }:
with lib;
let
cfg = config.services.beesd;
logLevels = { emerg = 0; alert = 1; crit = 2; err = 3; warning = 4; notice = 5; info = 6; debug = 7; };
fsOptions = with types; {
options.spec = 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.
</para>
<para>
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 = mkOption {
type = types.addCheck types.int (n: mod n 16 == 0);
default = 1024; # 1GB; default from upstream beesd script
description = ''
Hash table size in MB; must be a multiple of 16.
</para>
<para>
A larger ratio of index size to storage size means smaller blocks of
duplicate content are recognized.
</para>
<para>
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 = mkOption {
type = types.enum (attrNames logLevels ++ attrValues logLevels);
apply = v: if isString v then logLevels.${v} else v;
default = "info";
description = "Log verbosity (syslog keyword/level).";
};
options.workDir = 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 = mkOption {
type = listOf str;
default = [ ];
description = ''
Extra command-line options passed to the daemon. See upstream bees documentation.
'';
example = literalExpression ''
[ "--thread-count" "4" ]
'';
};
};
in
{
options.services.beesd = {
filesystems = mkOption {
type = with types; attrsOf (submodule fsOptions);
description = "BTRFS filesystems to run block-level deduplication on.";
default = { };
example = literalExpression ''
{
root = {
spec = "LABEL=root";
hashTableSizeMB = 2048;
verbosity = "crit";
extraOptions = [ "--loadavg-target" "5.0" ];
};
}
'';
};
};
config = {
systemd.services = mapAttrs'
(name: fs: nameValuePair "beesd@${name}" {
description = "Block-level BTRFS deduplication for %i";
after = [ "sysinit.target" ];
serviceConfig =
let
configOpts = [
fs.spec
"verbosity=${toString fs.verbosity}"
"idxSizeMB=${toString fs.hashTableSizeMB}"
"workDir=${fs.workDir}"
];
configOptsStr = escapeShellArgs configOpts;
in
{
# Values from https://github.com/Zygo/bees/blob/v0.6.5/scripts/beesd@.service.in
ExecStart = "${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${escapeShellArgs fs.extraOptions}";
ExecStopPost = "${pkgs.bees}/bin/bees-service-wrapper cleanup ${configOptsStr}";
CPUAccounting = true;
CPUSchedulingPolicy = "batch";
CPUWeight = 12;
IOSchedulingClass = "idle";
IOSchedulingPriority = 7;
IOWeight = 10;
KillMode = "control-group";
KillSignal = "SIGTERM";
MemoryAccounting = true;
Nice = 19;
Restart = "on-abnormal";
StartupCPUWeight = 25;
StartupIOWeight = 25;
SyslogIdentifier = "beesd"; # would otherwise be "bees-service-wrapper"
};
unitConfig.RequiresMountsFor = lib.mkIf (lib.hasPrefix "/" fs.spec) fs.spec;
wantedBy = [ "multi-user.target" ];
})
cfg.filesystems;
};
}

View file

@ -0,0 +1,179 @@
{ config, lib, pkgs, ... }:
with lib;
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 = mkEnableOption "Bepasty servers";
servers = mkOption {
default = {};
description = ''
configure a number of bepasty servers which will be started with
gunicorn.
'';
type = with types ; attrsOf (submodule ({ config, ... } : {
options = {
bind = mkOption {
type = types.str;
description = ''
Bind address to be used for this server.
'';
example = "0.0.0.0:8000";
default = "127.0.0.1:8000";
};
dataDir = mkOption {
type = types.str;
description = ''
Path to the directory where the pastes will be saved to
'';
default = default_home+"/data";
};
defaultPermissions = mkOption {
type = types.str;
description = ''
default permissions for all unauthenticated accesses.
'';
example = "read,create,delete";
default = "read";
};
extraConfig = mkOption {
type = 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 = mkOption {
type = 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</option>
which takes precedence over <option>secretKey</option>.
'';
default = "";
};
secretKeyFile = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
A file that contains the server secret for safe session cookies, must be set.
<option>secretKeyFile</option> takes precedence over <option>secretKey</option>.
Warning: when <option>secretKey</option> is non-empty <option>secretKeyFile</option>
defaults to a file in the WORLD-READABLE Nix store containing that secret.
'';
};
workDir = mkOption {
type = types.str;
description = ''
Path to the working directory (used for config and pidfile).
Defaults to the users home directory.
'';
default = default_home;
};
};
config = {
secretKeyFile = mkDefault (
if config.secretKey != ""
then toString (pkgs.writeTextFile {
name = "bepasty-secret-key";
text = config.secretKey;
})
else null
);
};
}));
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ bepasty ];
# creates gunicorn systemd service for each configured server
systemd.services = mapAttrs' (name: server:
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,86 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.calibre-server;
in
{
imports = [
(mkChangedOptionModule [ "services" "calibre-server" "libraryDir" ] [ "services" "calibre-server" "libraries" ]
(config:
let libraryDir = getAttrFromPath [ "services" "calibre-server" "libraryDir" ] config;
in [ libraryDir ]
)
)
];
###### interface
options = {
services.calibre-server = {
enable = mkEnableOption "calibre-server";
libraries = mkOption {
description = ''
The directories of the libraries to serve. They must be readable for the user under which the server runs.
'';
type = types.listOf types.path;
};
user = mkOption {
description = "The user under which calibre-server runs.";
type = types.str;
default = "calibre-server";
};
group = mkOption {
description = "The group under which calibre-server runs.";
type = types.str;
default = "calibre-server";
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.calibre-server = {
description = "Calibre Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Restart = "always";
ExecStart = "${pkgs.calibre}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries}";
};
};
environment.systemPackages = [ pkgs.calibre ];
users.users = 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 = optionalAttrs (cfg.group == "calibre-server") {
calibre-server = {
gid = config.ids.gids.calibre-server;
};
};
};
}

View file

@ -0,0 +1,37 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.canto-daemon;
in {
##### interface
options = {
services.canto-daemon = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the canto RSS daemon.";
};
};
};
##### implementation
config = 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,82 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.cfdyndns;
in
{
imports = [
(mkRemovedOptionModule
[ "services" "cfdyndns" "apikey" ]
"Use services.cfdyndns.apikeyFile instead.")
];
options = {
services.cfdyndns = {
enable = mkEnableOption "Cloudflare Dynamic DNS Client";
email = mkOption {
type = types.str;
description = ''
The email address to use to authenticate to CloudFlare.
'';
};
apikeyFile = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
The path to a file containing the API Key
used to authenticate with CloudFlare.
'';
};
records = mkOption {
default = [];
example = [ "host.tld" ];
type = types.listOf types.str;
description = ''
The records to update in CloudFlare.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.cfdyndns = {
description = "CloudFlare Dynamic DNS Client";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
startAt = "*:0/5";
serviceConfig = {
Type = "simple";
User = config.ids.uids.cfdyndns;
Group = config.ids.gids.cfdyndns;
};
environment = {
CLOUDFLARE_EMAIL="${cfg.email}";
CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}";
};
script = ''
${optionalString (cfg.apikeyFile != null) ''
export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})"
''}
${pkgs.cfdyndns}/bin/cfdyndns
'';
};
users.users = {
cfdyndns = {
group = "cfdyndns";
uid = config.ids.uids.cfdyndns;
};
};
users.groups = {
cfdyndns = {
gid = config.ids.gids.cfdyndns;
};
};
};
}

View file

@ -0,0 +1,148 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.cgminer;
convType = with builtins;
v: if isBool v then boolToString v else toString v;
mergedHwConfig =
mapAttrsToList (n: v: ''"${n}": "${(concatStringsSep "," (map convType v))}"'')
(foldAttrs (n: a: [n] ++ a) [] cfg.hardware);
mergedConfig = with builtins;
mapAttrsToList (n: v: ''"${n}": ${if isBool v then "" else ''"''}${convType v}${if isBool v then "" else ''"''}'')
cfg.config;
cgminerConfig = pkgs.writeText "cgminer.conf" ''
{
${concatStringsSep ",\n" mergedHwConfig},
${concatStringsSep ",\n" mergedConfig},
"pools": [
${concatStringsSep ",\n"
(map (v: ''{"url": "${v.url}", "user": "${v.user}", "pass": "${v.pass}"}'')
cfg.pools)}]
}
'';
in
{
###### interface
options = {
services.cgminer = {
enable = mkEnableOption "cgminer, an ASIC/FPGA/GPU miner for bitcoin and litecoin";
package = mkOption {
default = pkgs.cgminer;
defaultText = literalExpression "pkgs.cgminer";
description = "Which cgminer derivation to use.";
type = types.package;
};
user = mkOption {
type = types.str;
default = "cgminer";
description = "User account under which cgminer runs";
};
pools = mkOption {
default = []; # Run benchmark
type = types.listOf (types.attrsOf types.str);
description = "List of pools where to mine";
example = [{
url = "http://p2pool.org:9332";
username = "17EUZxTvs9uRmPsjPZSYUU3zCz9iwstudk";
password="X";
}];
};
hardware = mkOption {
default = []; # Run without options
type = types.listOf (types.attrsOf (types.either types.str 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 = mkOption {
default = {};
type = types.attrsOf (types.either types.bool 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 = mkIf config.services.cgminer.enable {
users.users = optionalAttrs (cfg.user == "cgminer") {
cgminer = {
isSystemUser = true;
group = "cgminer";
description = "Cgminer user";
};
};
users.groups = 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,31 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.clipcat;
in {
options.services.clipcat= {
enable = mkEnableOption "Clipcat clipboard daemon";
package = mkOption {
type = types.package;
default = pkgs.clipcat;
defaultText = literalExpression "pkgs.clipcat";
description = "clipcat derivation to use.";
};
};
config = 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,31 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.clipmenu;
in {
options.services.clipmenu = {
enable = mkEnableOption "clipmenu, the clipboard management daemon";
package = mkOption {
type = types.package;
default = pkgs.clipmenu;
defaultText = literalExpression "pkgs.clipmenu";
description = "clipmenu derivation to use.";
};
};
config = 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,90 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.confd;
confdConfig = ''
backend = "${cfg.backend}"
confdir = "${cfg.confDir}"
interval = ${toString cfg.interval}
nodes = [ ${concatMapStringsSep "," (s: ''"${s}"'') cfg.nodes}, ]
prefix = "${cfg.prefix}"
log-level = "${cfg.logLevel}"
watch = ${boolToString cfg.watch}
'';
in {
options.services.confd = {
enable = mkEnableOption "confd service";
backend = mkOption {
description = "Confd config storage backend to use.";
default = "etcd";
type = types.enum ["etcd" "consul" "redis" "zookeeper"];
};
interval = mkOption {
description = "Confd check interval.";
default = 10;
type = types.int;
};
nodes = mkOption {
description = "Confd list of nodes to connect to.";
default = [ "http://127.0.0.1:2379" ];
type = types.listOf types.str;
};
watch = mkOption {
description = "Confd, whether to watch etcd config for changes.";
default = true;
type = types.bool;
};
prefix = mkOption {
description = "The string to prefix to keys.";
default = "/";
type = types.path;
};
logLevel = mkOption {
description = "Confd log level.";
default = "info";
type = types.enum ["info" "debug"];
};
confDir = mkOption {
description = "The path to the confd configs.";
default = "/etc/confd";
type = types.path;
};
package = mkOption {
description = "Confd package to use.";
default = pkgs.confd;
defaultText = literalExpression "pkgs.confd";
type = types.package;
};
};
config = 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 = mkIf (cfg.backend == "etcd") (mkDefault true);
};
}

View file

@ -0,0 +1,66 @@
{ config, lib, pkgs, ... }:
with lib;
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 = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the cpuminer cryptonight miner.
'';
};
url = mkOption {
type = types.str;
description = "URL of mining server";
};
user = mkOption {
type = types.str;
description = "Username for mining server";
};
pass = mkOption {
type = types.str;
default = "x";
description = "Password for mining server";
};
threads = mkOption {
type = types.int;
default = 0;
description = "Number of miner threads, defaults to available processors";
};
};
};
config = 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,297 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.dendrite;
settingsFormat = pkgs.formats.yaml { };
configurationYaml = settingsFormat.generate "dendrite.yaml" cfg.settings;
workingDir = "/var/lib/dendrite";
in
{
options.services.dendrite = {
enable = lib.mkEnableOption "matrix.org dendrite";
httpPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = 8008;
description = ''
The port to listen for HTTP requests on.
'';
};
httpsPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
description = ''
The port to listen for HTTPS requests on.
'';
};
tlsCert = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/var/lib/dendrite/server.cert";
default = null;
description = ''
The path to the TLS certificate.
<programlisting>
nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
</programlisting>
'';
};
tlsKey = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/var/lib/dendrite/server.key";
default = null;
description = ''
The path to the TLS key.
<programlisting>
nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
</programlisting>
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/var/lib/dendrite/registration_secret";
default = null;
description = ''
Environment file as defined in <citerefentry>
<refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
</citerefentry>.
Secrets may be passed to the service without adding them to the world-readable
Nix store, by specifying placeholder variables as the option value in Nix and
setting these variables accordingly in the environment file. Currently only used
for the registration secret to allow secure registration when
client_api.registration_disabled is true.
<programlisting>
# snippet of dendrite-related config
services.dendrite.settings.client_api.registration_shared_secret = "$REGISTRATION_SHARED_SECRET";
</programlisting>
<programlisting>
# content of the environment file
REGISTRATION_SHARED_SECRET=verysecretpassword
</programlisting>
Note that this file needs to be available on the host on which
<literal>dendrite</literal> is running.
'';
};
loadCredential = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "private_key:/path/to/my_private_key" ];
description = ''
This can be used to pass secrets to the systemd service without adding them to
the nix store.
To use the example setting, see the example of
<option>services.dendrite.settings.global.private_key</option>.
See the LoadCredential section of systemd.exec manual for more information.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.global = {
server_name = lib.mkOption {
type = lib.types.str;
example = "example.com";
description = ''
The domain name of the server, with optional explicit port.
This is used by remote servers to connect to this server.
This is also the last part of your UserID.
'';
};
private_key = lib.mkOption {
type = lib.types.either
lib.types.path
(lib.types.strMatching "^\\$CREDENTIALS_DIRECTORY/.+");
example = "$CREDENTIALS_DIRECTORY/private_key";
description = ''
The path to the signing private key file, used to sign
requests and events.
<programlisting>
nix-shell -p dendrite --command "generate-keys --private-key matrix_key.pem"
</programlisting>
'';
};
trusted_third_party_id_servers = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [ "matrix.org" ];
default = [ "matrix.org" "vector.im" ];
description = ''
Lists of domains that the server will trust as identity
servers to verify third party identifiers such as phone
numbers and email addresses
'';
};
};
options.app_service_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:federationapi.db";
description = ''
Database for the Appservice API.
'';
};
};
options.client_api = {
registration_disabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to disable user registration to the server
without the shared secret.
'';
};
};
options.federation_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:federationapi.db";
description = ''
Database for the Federation API.
'';
};
};
options.key_server.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:keyserver.db";
description = ''
Database for the Key Server (for end-to-end encryption).
'';
};
};
options.media_api = {
database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:mediaapi.db";
description = ''
Database for the Media API.
'';
};
};
base_path = lib.mkOption {
type = lib.types.str;
default = "${workingDir}/media_store";
description = ''
Storage path for uploaded media.
'';
};
};
options.room_server.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:roomserver.db";
description = ''
Database for the Room Server.
'';
};
};
options.sync_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:syncserver.db";
description = ''
Database for the Sync API.
'';
};
};
options.user_api = {
account_database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:userapi_accounts.db";
description = ''
Database for the User API, accounts.
'';
};
};
device_database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:userapi_devices.db";
description = ''
Database for the User API, devices.
'';
};
};
};
options.mscs = {
database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:mscs.db";
description = ''
Database for exerimental MSC's.
'';
};
};
};
};
default = { };
description = ''
Configuration for dendrite, see:
<link xlink:href="https://github.com/matrix-org/dendrite/blob/master/dendrite-config.yaml"/>
for available options with which to populate settings.
'';
};
openRegistration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow open registration without secondary verification (reCAPTCHA).
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [{
assertion = cfg.httpsPort != null -> (cfg.tlsCert != null && cfg.tlsKey != null);
message = ''
If Dendrite is configured to use https, tlsCert and tlsKey must be provided.
nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
'';
}];
systemd.services.dendrite = {
description = "Dendrite Matrix homeserver";
after = [
"network.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = "dendrite";
WorkingDirectory = workingDir;
RuntimeDirectory = "dendrite";
RuntimeDirectoryMode = "0700";
LimitNOFILE = 65535;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
LoadCredential = cfg.loadCredential;
ExecStartPre = ''
${pkgs.envsubst}/bin/envsubst \
-i ${configurationYaml} \
-o /run/dendrite/dendrite.yaml
'';
ExecStart = lib.strings.concatStringsSep " " ([
"${pkgs.dendrite}/bin/dendrite-monolith-server"
"--config /run/dendrite/dendrite.yaml"
] ++ lib.optionals (cfg.httpPort != null) [
"--http-bind-address :${builtins.toString cfg.httpPort}"
] ++ lib.optionals (cfg.httpsPort != null) [
"--https-bind-address :${builtins.toString cfg.httpsPort}"
"--tls-cert ${cfg.tlsCert}"
"--tls-key ${cfg.tlsKey}"
] ++ lib.optionals cfg.openRegistration [
"--really-enable-open-registration"
]);
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "on-failure";
};
};
};
meta.maintainers = lib.teams.matrix.members;
}

View file

@ -0,0 +1,25 @@
{ pkgs, config, lib, ... }:
with lib;
let
cfg = config.services.devmon;
in {
options = {
services.devmon = {
enable = mkEnableOption "devmon, an automatic device mounting daemon";
};
};
config = 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,65 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.dictd;
in
{
###### interface
options = {
services.dictd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the DICT.org dictionary server.
'';
};
DBs = mkOption {
type = types.listOf types.package;
default = with pkgs.dictdDBs; [ wiktionary wordnet ];
defaultText = literalExpression "with pkgs.dictdDBs; [ wiktionary wordnet ]";
example = 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 mkIf cfg.enable {
# get the command line client on system path to make some use of the service
environment.systemPackages = [ pkgs.dict ];
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"; };
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,98 @@
# Disnix server
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.disnix;
in
{
###### interface
options = {
services.disnix = {
enable = mkEnableOption "Disnix";
enableMultiUser = mkOption {
type = types.bool;
default = true;
description = "Whether to support multi-user mode by enabling the Disnix D-Bus service";
};
useWebServiceInterface = mkEnableOption "the DisnixWebService interface running on Apache Tomcat";
package = mkOption {
type = types.path;
description = "The Disnix package";
default = pkgs.disnix;
defaultText = literalExpression "pkgs.disnix";
};
enableProfilePath = mkEnableOption "exposing the Disnix profiles in the system's PATH";
profiles = mkOption {
type = types.listOf types.str;
default = [ "default" ];
description = "Names of the Disnix profiles to expose in the system's PATH";
};
};
};
###### implementation
config = mkIf cfg.enable {
dysnomia.enable = true;
environment.systemPackages = [ pkgs.disnix ] ++ 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 = "${optionalString cfg.useWebServiceInterface "-Djava.library.path=${pkgs.libmatthew_java}/lib/jni"} ";
services.tomcat.sharedLibs = optional cfg.useWebServiceInterface "${pkgs.DisnixWebService}/share/java/DisnixConnection.jar"
++ optional cfg.useWebServiceInterface "${pkgs.dbus_java}/share/java/dbus.jar";
services.tomcat.webapps = optional cfg.useWebServiceInterface pkgs.DisnixWebService;
users.groups.disnix.gid = config.ids.gids.disnix;
systemd.services = {
disnix = mkIf cfg.enableMultiUser {
description = "Disnix server";
wants = [ "dysnomia.target" ];
wantedBy = [ "multi-user.target" ];
after = [ "dbus.service" ]
++ optional config.services.httpd.enable "httpd.service"
++ optional config.services.mysql.enable "mysql.service"
++ optional config.services.postgresql.enable "postgresql.service"
++ optional config.services.tomcat.enable "tomcat.service"
++ optional config.services.svnserve.enable "svnserve.service"
++ optional config.services.mongodb.enable "mongodb.service"
++ optional config.services.influxdb.enable "influxdb.service";
restartIfChanged = false;
path = [ config.nix.package cfg.package config.dysnomia.package "/run/current-system/sw" ];
environment = {
HOME = "/root";
}
// (if config.environment.variables ? DYSNOMIA_CONTAINERS_PATH then { inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH; } else {})
// (if config.environment.variables ? DYSNOMIA_MODULES_PATH then { inherit (config.environment.variables) DYSNOMIA_MODULES_PATH; } else {});
serviceConfig.ExecStart = "${cfg.package}/bin/disnix-service";
};
};
};
}

View file

@ -0,0 +1,159 @@
{ config, lib, pkgs, ... }:
with lib;
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;
} // (if cfg.storagePath != null
then { filesystem.rootdirectory = cfg.storagePath; }
else {});
http = {
addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
headers.X-Content-Type-Options = ["nosniff"];
};
health.storagedriver = {
enabled = true;
interval = "10s";
threshold = 3;
};
};
registryConfig.redis = 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 = pkgs.writeText "docker-registry-config.yml" (builtins.toJSON (recursiveUpdate registryConfig cfg.extraConfig));
in {
options.services.dockerRegistry = {
enable = mkEnableOption "Docker Registry";
listenAddress = mkOption {
description = "Docker registry host or ip to bind to.";
default = "127.0.0.1";
type = types.str;
};
port = mkOption {
description = "Docker registry port to bind to.";
default = 5000;
type = types.port;
};
storagePath = mkOption {
type = types.nullOr 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 = mkOption {
type = types.bool;
default = false;
description = "Enable delete for manifests and blobs.";
};
enableRedisCache = mkEnableOption "redis as blob cache";
redisUrl = mkOption {
type = types.str;
default = "localhost:6379";
description = "Set redis host and port.";
};
redisPassword = mkOption {
type = types.str;
default = "";
description = "Set redis password.";
};
extraConfig = mkOption {
description = ''
Docker extra registry configuration via environment variables.
'';
default = {};
type = types.attrs;
};
enableGarbageCollect = mkEnableOption "garbage collect";
garbageCollectDates = mkOption {
default = "daily";
type = types.str;
description = ''
Specification (in the format described by
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>) of the time at
which the garbage collect will occur.
'';
};
};
config = mkIf cfg.enable {
systemd.services.docker-registry = {
description = "Docker Container Registry";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
script = ''
${pkgs.docker-distribution}/bin/registry serve ${configFile}
'';
serviceConfig = {
User = "docker-registry";
WorkingDirectory = cfg.storagePath;
AmbientCapabilities = 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 = ''
${pkgs.docker-distribution}/bin/registry garbage-collect ${configFile}
/run/current-system/systemd/bin/systemctl restart docker-registry.service
'';
startAt = optional cfg.enableGarbageCollect cfg.garbageCollectDates;
};
users.users.docker-registry =
(if cfg.storagePath != null
then {
createHome = true;
home = cfg.storagePath;
}
else {}) // {
group = "docker-registry";
isSystemUser = true;
};
users.groups.docker-registry = {};
};
}

View file

@ -0,0 +1,51 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.services.domoticz;
pkgDesc = "Domoticz home automation";
in {
options = {
services.domoticz = {
enable = mkEnableOption pkgDesc;
bind = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address to bind to.";
};
port = mkOption {
type = types.int;
default = 8080;
description = "Port to bind to for HTTP, set to 0 to disable HTTP.";
};
};
};
config = mkIf cfg.enable {
systemd.services."domoticz" = {
description = pkgDesc;
wantedBy = [ "multi-user.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,39 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.duckling;
in {
options = {
services.duckling = {
enable = mkEnableOption "duckling";
port = mkOption {
type = types.port;
default = 8080;
description = ''
Port on which duckling will run.
'';
};
};
};
config = 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,73 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.dwm-status;
order = concatMapStringsSep "," (feature: ''"${feature}"'') cfg.order;
configFile = pkgs.writeText "dwm-status.toml" ''
order = [${order}]
${cfg.extraConfig}
'';
in
{
###### interface
options = {
services.dwm-status = {
enable = mkEnableOption "dwm-status user service";
package = mkOption {
type = types.package;
default = pkgs.dwm-status;
defaultText = literalExpression "pkgs.dwm-status";
example = literalExpression "pkgs.dwm-status.override { enableAlsaUtils = false; }";
description = ''
Which dwm-status package to use.
'';
};
order = mkOption {
type = types.listOf (types.enum [ "audio" "backlight" "battery" "cpu_load" "network" "time" ]);
description = ''
List of enabled features in order.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra config in TOML format.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
services.upower.enable = elem "battery" cfg.order;
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}";
};
};
}

View file

@ -0,0 +1,265 @@
{pkgs, lib, config, ...}:
with lib;
let
cfg = config.dysnomia;
printProperties = properties:
concatMapStrings (propertyName:
let
property = properties.${propertyName};
in
if 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
${concatMapStrings (containerName:
let
containerProperties = cfg.containers.${containerName};
in
''
cat > ${containerName} <<EOF
${printProperties containerProperties}
type=${containerName}
EOF
''
) (builtins.attrNames cfg.containers)}
'';
};
linkMutableComponents = {containerName}:
''
mkdir ${containerName}
${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
${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 = {
dysnomia = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable Dysnomia";
};
enableAuthentication = mkOption {
type = types.bool;
default = false;
description = "Whether to publish privacy-sensitive authentication credentials";
};
package = mkOption {
type = types.path;
description = "The Dysnomia package";
};
properties = mkOption {
description = "An attribute set in which each attribute represents a machine property. Optionally, these values can be shell substitutions.";
default = {};
type = types.attrs;
};
containers = mkOption {
description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
default = {};
type = types.attrsOf types.attrs;
};
components = mkOption {
description = "An atttribute 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 = types.attrsOf types.attrs;
};
extraContainerProperties = mkOption {
description = "An attribute set providing additional container settings in addition to the default properties";
default = {};
type = types.attrs;
};
extraContainerPaths = mkOption {
description = "A list of paths containing additional container configurations that are added to the search folders";
default = [];
type = types.listOf types.path;
};
extraModulePaths = mkOption {
description = "A list of paths containing additional modules that are added to the search folders";
default = [];
type = types.listOf types.path;
};
enableLegacyModules = mkOption {
type = types.bool;
default = true;
description = "Whether to enable Dysnomia legacy process and wrapper modules";
};
};
};
config = 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 ];
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;
});
dysnomia.properties = {
hostname = config.networking.hostName;
inherit (config.nixpkgs.localSystem) 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"
]
++ optional (dysnomiaFlags.enableApacheWebApplication) "apache-webapplication"
++ optional (dysnomiaFlags.enableAxis2WebService) "axis2-webservice"
++ optional (dysnomiaFlags.enableDockerContainer) "docker-container"
++ optional (dysnomiaFlags.enableEjabberdDump) "ejabberd-dump"
++ optional (dysnomiaFlags.enableInfluxDatabase) "influx-database"
++ optional (dysnomiaFlags.enableMySQLDatabase) "mysql-database"
++ optional (dysnomiaFlags.enablePostgreSQLDatabase) "postgresql-database"
++ optional (dysnomiaFlags.enableTomcatWebApplication) "tomcat-webapplication"
++ optional (dysnomiaFlags.enableMongoDatabase) "mongo-database"
++ optional (dysnomiaFlags.enableSubversionRepository) "subversion-repository";
};
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.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,104 @@
{ config, lib, pkgs, ... }:
with lib;
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 = (${concatMapStringsSep "," (name: "'${name}'") instanceCfg.admins})
BOT_IDENTITY = ${builtins.toJSON instanceCfg.identity}
${instanceCfg.extraConfig}
'';
in {
options = {
services.errbot.instances = mkOption {
default = {};
description = "Errbot instance configs";
type = types.attrsOf (types.submodule {
options = {
dataDir = mkOption {
type = types.nullOr types.path;
default = null;
description = "Data directory for errbot instance.";
};
plugins = mkOption {
type = types.listOf types.package;
default = [];
description = "List of errbot plugin derivations.";
};
logLevel = mkOption {
type = types.str;
default = "INFO";
description = "Errbot log level";
};
admins = mkOption {
type = types.listOf types.str;
default = [];
description = "List of identifiers of errbot admins.";
};
backend = mkOption {
type = types.str;
default = "XMPP";
description = "Errbot backend name.";
};
identity = mkOption {
type = types.attrs;
description = "Errbot identity configuration";
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "String to be appended to the config verbatim";
};
};
});
};
};
config = mkIf (cfg.instances != {}) {
users.users.errbot = {
group = "errbot";
isSystemUser = true;
};
users.groups.errbot = {};
systemd.services = mapAttrs' (name: instanceCfg: 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,205 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.etcd;
opt = options.services.etcd;
in {
options.services.etcd = {
enable = mkOption {
description = "Whether to enable etcd.";
default = false;
type = types.bool;
};
name = mkOption {
description = "Etcd unique node name.";
default = config.networking.hostName;
defaultText = literalExpression "config.networking.hostName";
type = types.str;
};
advertiseClientUrls = mkOption {
description = "Etcd list of this member's client URLs to advertise to the rest of the cluster.";
default = cfg.listenClientUrls;
defaultText = literalExpression "config.${opt.listenClientUrls}";
type = types.listOf types.str;
};
listenClientUrls = mkOption {
description = "Etcd list of URLs to listen on for client traffic.";
default = ["http://127.0.0.1:2379"];
type = types.listOf types.str;
};
listenPeerUrls = mkOption {
description = "Etcd list of URLs to listen on for peer traffic.";
default = ["http://127.0.0.1:2380"];
type = types.listOf types.str;
};
initialAdvertisePeerUrls = mkOption {
description = "Etcd list of this member's peer URLs to advertise to rest of the cluster.";
default = cfg.listenPeerUrls;
defaultText = literalExpression "config.${opt.listenPeerUrls}";
type = types.listOf types.str;
};
initialCluster = mkOption {
description = "Etcd initial cluster configuration for bootstrapping.";
default = ["${cfg.name}=http://127.0.0.1:2380"];
defaultText = literalExpression ''["''${config.${opt.name}}=http://127.0.0.1:2380"]'';
type = types.listOf types.str;
};
initialClusterState = mkOption {
description = "Etcd initial cluster configuration for bootstrapping.";
default = "new";
type = types.enum ["new" "existing"];
};
initialClusterToken = mkOption {
description = "Etcd initial cluster token for etcd cluster during bootstrap.";
default = "etcd-cluster";
type = types.str;
};
discovery = mkOption {
description = "Etcd discovery url";
default = "";
type = types.str;
};
clientCertAuth = mkOption {
description = "Whether to use certs for client authentication";
default = false;
type = types.bool;
};
trustedCaFile = mkOption {
description = "Certificate authority file to use for clients";
default = null;
type = types.nullOr types.path;
};
certFile = mkOption {
description = "Cert file to use for clients";
default = null;
type = types.nullOr types.path;
};
keyFile = mkOption {
description = "Key file to use for clients";
default = null;
type = types.nullOr types.path;
};
peerCertFile = mkOption {
description = "Cert file to use for peer to peer communication";
default = cfg.certFile;
defaultText = literalExpression "config.${opt.certFile}";
type = types.nullOr types.path;
};
peerKeyFile = mkOption {
description = "Key file to use for peer to peer communication";
default = cfg.keyFile;
defaultText = literalExpression "config.${opt.keyFile}";
type = types.nullOr types.path;
};
peerTrustedCaFile = mkOption {
description = "Certificate authority file to use for peer to peer communication";
default = cfg.trustedCaFile;
defaultText = literalExpression "config.${opt.trustedCaFile}";
type = types.nullOr types.path;
};
peerClientCertAuth = mkOption {
description = "Whether to check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA";
default = false;
type = types.bool;
};
extraConf = mkOption {
description = ''
Etcd extra configuration. See
<link xlink:href='https://github.com/coreos/etcd/blob/master/Documentation/op-guide/configuration.md#configuration-flags' />
'';
type = types.attrsOf types.str;
default = {};
example = literalExpression ''
{
"CORS" = "*";
"NAME" = "default-name";
"MAX_RESULT_BUFFER" = "1024";
"MAX_CLUSTER_SIZE" = "9";
"MAX_RETRY_ATTEMPTS" = "3";
}
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/etcd";
description = "Etcd data directory.";
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0700 etcd - - -"
];
systemd.services.etcd = {
description = "etcd key-value store";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = (filterAttrs (n: v: v != null) {
ETCD_NAME = cfg.name;
ETCD_DISCOVERY = cfg.discovery;
ETCD_DATA_DIR = cfg.dataDir;
ETCD_ADVERTISE_CLIENT_URLS = concatStringsSep "," cfg.advertiseClientUrls;
ETCD_LISTEN_CLIENT_URLS = concatStringsSep "," cfg.listenClientUrls;
ETCD_LISTEN_PEER_URLS = concatStringsSep "," cfg.listenPeerUrls;
ETCD_INITIAL_ADVERTISE_PEER_URLS = concatStringsSep "," cfg.initialAdvertisePeerUrls;
ETCD_PEER_TRUSTED_CA_FILE = cfg.peerTrustedCaFile;
ETCD_PEER_CERT_FILE = cfg.peerCertFile;
ETCD_PEER_KEY_FILE = cfg.peerKeyFile;
ETCD_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
ETCD_TRUSTED_CA_FILE = cfg.trustedCaFile;
ETCD_CERT_FILE = cfg.certFile;
ETCD_KEY_FILE = cfg.keyFile;
}) // (optionalAttrs (cfg.discovery == ""){
ETCD_INITIAL_CLUSTER = concatStringsSep "," cfg.initialCluster;
ETCD_INITIAL_CLUSTER_STATE = cfg.initialClusterState;
ETCD_INITIAL_CLUSTER_TOKEN = cfg.initialClusterToken;
}) // (mapAttrs' (n: v: nameValuePair "ETCD_${n}" v) cfg.extraConf);
unitConfig = {
Documentation = "https://github.com/coreos/etcd";
};
serviceConfig = {
Type = "notify";
ExecStart = "${pkgs.etcd}/bin/etcd";
User = "etcd";
LimitNOFILE = 40000;
};
};
environment.systemPackages = [ pkgs.etcd ];
users.users.etcd = {
isSystemUser = true;
group = "etcd";
description = "Etcd daemon user";
home = cfg.dataDir;
};
users.groups.etcd = {};
};
}

View file

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

View file

@ -0,0 +1,92 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.etesync-dav;
in
{
options.services.etesync-dav = {
enable = mkEnableOption "etesync-dav";
host = mkOption {
type = types.str;
default = "localhost";
description = "The server host address.";
};
port = mkOption {
type = types.port;
default = 37358;
description = "The server host port.";
};
apiUrl = mkOption {
type = types.str;
default = "https://api.etesync.com/";
description = "The url to the etesync API.";
};
openFirewall = mkOption {
default = false;
type = types.bool;
description = "Whether to open the firewall for the specified port.";
};
sslCertificate = mkOption {
type = types.nullOr 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 = mkOption {
type = types.nullOr 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 = mkIf cfg.enable {
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
systemd.services.etesync-dav = {
description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
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 = mkIf (cfg.sslCertificate != null || cfg.sslCertificateKey != null) (
pkgs.writers.writeBash "etesync-dav-copy-keys" ''
${optionalString (cfg.sslCertificate != null) ''
cp ${toString cfg.sslCertificate} $STATE_DIRECTORY/etesync.crt
''}
${optionalString (cfg.sslCertificateKey != null) ''
cp ${toString cfg.sslCertificateKey} $STATE_DIRECTORY/etesync.key
''}
''
);
Restart = "on-failure";
RestartSec = "30min 1s";
};
};
};
}

View file

@ -0,0 +1,117 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ethminer;
poolUrl = escapeShellArg "stratum1+tcp://${cfg.wallet}@${cfg.pool}:${toString cfg.stratumPort}/${cfg.rig}/${cfg.registerMail}";
in
{
###### interface
options = {
services.ethminer = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable ethminer ether mining.";
};
recheckInterval = mkOption {
type = types.ints.unsigned;
default = 2000;
description = "Interval in milliseconds between farm rechecks.";
};
toolkit = mkOption {
type = types.enum [ "cuda" "opencl" ];
default = "cuda";
description = "Cuda or opencl toolkit.";
};
apiPort = mkOption {
type = types.int;
default = -3333;
description = "Ethminer api port. minus sign puts api in read-only mode.";
};
wallet = mkOption {
type = types.str;
example = "0x0123456789abcdef0123456789abcdef01234567";
description = "Ethereum wallet address.";
};
pool = mkOption {
type = types.str;
example = "eth-us-east1.nanopool.org";
description = "Mining pool address.";
};
stratumPort = mkOption {
type = types.port;
default = 9999;
description = "Stratum protocol tcp port.";
};
rig = mkOption {
type = types.str;
default = "mining-rig-name";
description = "Mining rig name.";
};
registerMail = mkOption {
type = types.str;
example = "email%40example.org";
description = "Url encoded email address to register with pool.";
};
maxPower = mkOption {
type = types.ints.unsigned;
default = 113;
description = "Miner max watt usage.";
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.ethminer = {
path = optional (cfg.toolkit == "cuda") [ pkgs.cudaPackages.cudatoolkit ];
description = "ethminer ethereum mining service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
ExecStartPre = "${pkgs.ethminer}/bin/.ethminer-wrapped --list-devices";
ExecStartPost = optional (cfg.toolkit == "cuda") "+${getBin config.boot.kernelPackages.nvidia_x11}/bin/nvidia-smi -pl ${toString cfg.maxPower}";
Restart = "always";
};
environment = mkIf (cfg.toolkit == "cuda") {
LD_LIBRARY_PATH = "${config.boot.kernelPackages.nvidia_x11}/lib";
};
script = ''
${pkgs.ethminer}/bin/.ethminer-wrapped \
--farm-recheck ${toString cfg.recheckInterval} \
--report-hashrate \
--${cfg.toolkit} \
--api-port ${toString cfg.apiPort} \
--pool ${poolUrl}
'';
};
};
}

View file

@ -0,0 +1,422 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.exhibitor;
opt = options.services.exhibitor;
exhibitorConfig = ''
zookeeper-install-directory=${cfg.baseDir}/zookeeper
zookeeper-data-directory=${cfg.zkDataDir}
zookeeper-log-directory=${cfg.zkLogDir}
zoo-cfg-extra=${cfg.zkExtraCfg}
client-port=${toString cfg.zkClientPort}
connect-port=${toString cfg.zkConnectPort}
election-port=${toString cfg.zkElectionPort}
cleanup-period-ms=${toString cfg.zkCleanupPeriod}
servers-spec=${concatStringsSep "," cfg.zkServersSpec}
auto-manage-instances=${toString cfg.autoManageInstances}
${cfg.extraConf}
'';
# NB: toString rather than lib.boolToString on cfg.autoManageInstances is intended.
# Exhibitor tests if it's an integer not equal to 0, so the empty string (toString false)
# will operate in the same fashion as a 0.
configDir = pkgs.writeTextDir "exhibitor.properties" exhibitorConfig;
cliOptionsCommon = {
configtype = cfg.configType;
defaultconfig = "${configDir}/exhibitor.properties";
port = toString cfg.port;
hostname = cfg.hostname;
headingtext = if (cfg.headingText != null) then (lib.escapeShellArg cfg.headingText) else null;
nodemodification = lib.boolToString cfg.nodeModification;
configcheckms = toString cfg.configCheckMs;
jquerystyle = cfg.jqueryStyle;
loglines = toString cfg.logLines;
servo = lib.boolToString cfg.servo;
timeout = toString cfg.timeout;
};
s3CommonOptions = { s3region = cfg.s3Region; s3credentials = cfg.s3Credentials; };
cliOptionsPerConfig = {
s3 = {
s3config = "${cfg.s3Config.bucketName}:${cfg.s3Config.objectKey}";
s3configprefix = cfg.s3Config.configPrefix;
};
zookeeper = {
zkconfigconnect = concatStringsSep "," cfg.zkConfigConnect;
zkconfigexhibitorpath = cfg.zkConfigExhibitorPath;
zkconfigpollms = toString cfg.zkConfigPollMs;
zkconfigretry = "${toString cfg.zkConfigRetry.sleepMs}:${toString cfg.zkConfigRetry.retryQuantity}";
zkconfigzpath = cfg.zkConfigZPath;
zkconfigexhibitorport = toString cfg.zkConfigExhibitorPort; # NB: This might be null
};
file = {
fsconfigdir = cfg.fsConfigDir;
fsconfiglockprefix = cfg.fsConfigLockPrefix;
fsConfigName = fsConfigName;
};
none = {
noneconfigdir = configDir;
};
};
cliOptions = concatStringsSep " " (mapAttrsToList (k: v: "--${k} ${v}") (filterAttrs (k: v: v != null && v != "") (cliOptionsCommon //
cliOptionsPerConfig.${cfg.configType} //
s3CommonOptions //
optionalAttrs cfg.s3Backup { s3backup = "true"; } //
optionalAttrs cfg.fileSystemBackup { filesystembackup = "true"; }
)));
in
{
options = {
services.exhibitor = {
enable = mkOption {
type = types.bool;
default = false;
description = "
Whether to enable the exhibitor server.
";
};
# See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean
# General options for any type of config
port = mkOption {
type = types.int;
default = 8080;
description = ''
The port for exhibitor to listen on and communicate with other exhibitors.
'';
};
baseDir = mkOption {
type = types.str;
default = "/var/exhibitor";
description = ''
Baseline directory for exhibitor runtime config.
'';
};
configType = mkOption {
type = types.enum [ "file" "s3" "zookeeper" "none" ];
description = ''
Which configuration type you want to use. Additional config will be
required depending on which type you are using.
'';
};
hostname = mkOption {
type = types.nullOr types.str;
description = ''
Hostname to use and advertise
'';
default = null;
};
nodeModification = mkOption {
type = types.bool;
description = ''
Whether the Explorer UI will allow nodes to be modified (use with caution).
'';
default = true;
};
configCheckMs = mkOption {
type = types.int;
description = ''
Period (ms) to check for shared config updates.
'';
default = 30000;
};
headingText = mkOption {
type = types.nullOr types.str;
description = ''
Extra text to display in UI header
'';
default = null;
};
jqueryStyle = mkOption {
type = types.enum [ "red" "black" "custom" ];
description = ''
Styling used for the JQuery-based UI.
'';
default = "red";
};
logLines = mkOption {
type = types.int;
description = ''
Max lines of logging to keep in memory for display.
'';
default = 1000;
};
servo = mkOption {
type = types.bool;
description = ''
ZooKeeper will be queried once a minute for its state via the 'mntr' four
letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish
this data via JMX.
'';
default = false;
};
timeout = mkOption {
type = types.int;
description = ''
Connection timeout (ms) for ZK connections.
'';
default = 30000;
};
autoManageInstances = mkOption {
type = types.bool;
description = ''
Automatically manage ZooKeeper instances in the ensemble
'';
default = false;
};
zkDataDir = mkOption {
type = types.str;
default = "${cfg.baseDir}/zkData";
defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"'';
description = ''
The Zookeeper data directory
'';
};
zkLogDir = mkOption {
type = types.path;
default = "${cfg.baseDir}/zkLogs";
defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"'';
description = ''
The Zookeeper logs directory
'';
};
extraConf = mkOption {
type = types.str;
default = "";
description = ''
Extra Exhibitor configuration to put in the ZooKeeper config file.
'';
};
zkExtraCfg = mkOption {
type = types.str;
default = "initLimit=5&syncLimit=2&tickTime=2000";
description = ''
Extra options to pass into Zookeeper
'';
};
zkClientPort = mkOption {
type = types.int;
default = 2181;
description = ''
Zookeeper client port
'';
};
zkConnectPort = mkOption {
type = types.int;
default = 2888;
description = ''
The port to use for followers to talk to each other.
'';
};
zkElectionPort = mkOption {
type = types.int;
default = 3888;
description = ''
The port for Zookeepers to use for leader election.
'';
};
zkCleanupPeriod = mkOption {
type = types.int;
default = 0;
description = ''
How often (in milliseconds) to run the Zookeeper log cleanup task.
'';
};
zkServersSpec = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Zookeeper server spec for all servers in the ensemble.
'';
example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ];
};
# Backup options
s3Backup = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable backups to S3
'';
};
fileSystemBackup = mkOption {
type = types.bool;
default = false;
description = ''
Enables file system backup of ZooKeeper log files
'';
};
# Options for using zookeeper configType
zkConfigConnect = mkOption {
type = types.listOf types.str;
description = ''
The initial connection string for ZooKeeper shared config storage
'';
example = ["host1:2181" "host2:2181"];
};
zkConfigExhibitorPath = mkOption {
type = types.str;
description = ''
If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call
'';
default = "/";
};
zkConfigExhibitorPort = mkOption {
type = types.nullOr types.int;
description = ''
If the ZooKeeper shared config is also running Exhibitor, the port that
Exhibitor is listening on. IMPORTANT: if this value is not set it implies
that Exhibitor is not being used on the ZooKeeper shared config.
'';
};
zkConfigPollMs = mkOption {
type = types.int;
description = ''
The period in ms to check for changes in the config ensemble
'';
default = 10000;
};
zkConfigRetry = {
sleepMs = mkOption {
type = types.int;
default = 1000;
description = ''
Retry sleep time connecting to the ZooKeeper config
'';
};
retryQuantity = mkOption {
type = types.int;
default = 3;
description = ''
Retries connecting to the ZooKeeper config
'';
};
};
zkConfigZPath = mkOption {
type = types.str;
description = ''
The base ZPath that Exhibitor should use
'';
example = "/exhibitor/config";
};
# Config options for s3 configType
s3Config = {
bucketName = mkOption {
type = types.str;
description = ''
Bucket name to store config
'';
};
objectKey = mkOption {
type = types.str;
description = ''
S3 key name to store the config
'';
};
configPrefix = mkOption {
type = types.str;
description = ''
When using AWS S3 shared config files, the prefix to use for values such as locks
'';
default = "exhibitor-";
};
};
# The next two are used for either s3backup or s3 configType
s3Credentials = mkOption {
type = types.nullOr types.path;
description = ''
Optional credentials to use for s3backup or s3config. Argument is the path
to an AWS credential properties file with two properties:
com.netflix.exhibitor.s3.access-key-id and com.netflix.exhibitor.s3.access-secret-key
'';
default = null;
};
s3Region = mkOption {
type = types.nullOr types.str;
description = ''
Optional region for S3 calls
'';
default = null;
};
# Config options for file config type
fsConfigDir = mkOption {
type = types.path;
description = ''
Directory to store Exhibitor properties (cannot be used with s3config).
Exhibitor uses file system locks so you can specify a shared location
so as to enable complete ensemble management.
'';
};
fsConfigLockPrefix = mkOption {
type = types.str;
description = ''
A prefix for a locking mechanism used in conjunction with fsconfigdir
'';
default = "exhibitor-lock-";
};
fsConfigName = mkOption {
type = types.str;
description = ''
The name of the file to store config in
'';
default = "exhibitor.properties";
};
};
};
config = mkIf cfg.enable {
systemd.services.exhibitor = {
description = "Exhibitor Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
ZOO_LOG_DIR = cfg.baseDir;
};
serviceConfig = {
/***
Exhibitor is a bit un-nixy. It wants to present to you a user interface in order to
mutate the configuration of both itself and ZooKeeper, and to coordinate changes
among the members of the Zookeeper ensemble. I'm going for a different approach here,
which is to manage all the configuration via nix and have it write out the configuration
files that exhibitor will use, and to reduce the amount of inter-exhibitor orchestration.
***/
ExecStart = ''
${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions}
'';
User = "zookeeper";
PermissionsStartOnly = true;
};
# This is a bit wonky, but the reason for this is that Exhibitor tries to write to
# ${cfg.baseDir}/zookeeper/bin/../conf/zoo.cfg
# I want everything but the conf directory to be in the immutable nix store, and I want defaults
# from the nix store
# If I symlink the bin directory in, then bin/../ will resolve to the parent of the symlink in the
# immutable nix store. Bind mounting a writable conf over the existing conf might work, but it gets very
# messy with trying to copy the existing out into a mutable store.
# Another option is to try to patch upstream exhibitor, but the current package just pulls down the
# prebuild JARs off of Maven, rather than building them ourselves, as Maven support in Nix isn't
# very mature. So, it seems like a reasonable compromise is to just copy out of the immutable store
# just before starting the service, so we're running binaries from the immutable store, but we work around
# Exhibitor's desire to mutate its current installation.
preStart = ''
mkdir -m 0700 -p ${cfg.baseDir}/zookeeper
# Not doing a chown -R to keep the base ZK files owned by root
chown zookeeper ${cfg.baseDir} ${cfg.baseDir}/zookeeper
cp -Rf ${pkgs.zookeeper}/* ${cfg.baseDir}/zookeeper
chown -R zookeeper ${cfg.baseDir}/zookeeper/conf
chmod -R u+w ${cfg.baseDir}/zookeeper/conf
replace_what=$(echo ${pkgs.zookeeper} | sed 's/[\/&]/\\&/g')
replace_with=$(echo ${cfg.baseDir}/zookeeper | sed 's/[\/&]/\\&/g')
sed -i 's/'"$replace_what"'/'"$replace_with"'/g' ${cfg.baseDir}/zookeeper/bin/zk*.sh
'';
};
users.users.zookeeper = {
uid = config.ids.uids.zookeeper;
description = "Zookeeper daemon user";
home = cfg.baseDir;
};
};
}

View file

@ -0,0 +1,104 @@
# Felix server
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.felix;
in
{
###### interface
options = {
services.felix = {
enable = mkEnableOption "the Apache Felix OSGi service";
bundles = mkOption {
type = types.listOf types.package;
default = [ pkgs.felix_remoteshell ];
defaultText = literalExpression "[ pkgs.felix_remoteshell ]";
description = "List of bundles that should be activated on startup";
};
user = mkOption {
type = types.str;
default = "osgi";
description = "User account under which Apache Felix runs.";
};
group = mkOption {
type = types.str;
default = "osgi";
description = "Group account under which Apache Felix runs.";
};
};
};
###### implementation
config = 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,104 @@
{ config, lib, pkgs, ...}:
with lib;
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
${concatStringsSep "\n" (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 = mkEnableOption "FreeSWITCH";
enableReload = mkOption {
default = false;
type = types.bool;
description = ''
Issue the <literal>reloadxml</literal> command to FreeSWITCH when configuration directory changes (instead of restart).
See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Reloading">FreeSWITCH documentation</link> for more info.
The configuration directory is exposed at <filename>/etc/freeswitch</filename>.
See also <literal>systemd.services.*.restartIfChanged</literal>.
'';
};
configTemplate = mkOption {
type = types.path;
default = "${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
defaultText = literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/vanilla"'';
example = literalExpression ''"''${config.services.freeswitch.package}/share/freeswitch/conf/minimal"'';
description = ''
Configuration template to use.
See available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
You can also set your own configuration directory.
'';
};
configDir = mkOption {
type = with types; attrsOf path;
default = { };
example = 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 <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Default+Configuration">FreeSWITCH documentation</link> for more info.
Also check available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
'';
};
package = mkOption {
type = types.package;
default = pkgs.freeswitch;
defaultText = literalExpression "pkgs.freeswitch";
description = ''
FreeSWITCH package.
'';
};
};
};
config = mkIf cfg.enable {
environment.etc.freeswitch = mkIf cfg.enableReload {
source = configDirectory;
};
systemd.services.freeswitch-config-reload = 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,46 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.fstrim;
in {
options = {
services.fstrim = {
enable = mkEnableOption "periodic SSD TRIM of mounted partitions in background";
interval = mkOption {
type = 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
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
};
};
config = mkIf cfg.enable {
systemd.packages = [ pkgs.util-linux ];
systemd.timers.fstrim = {
timerConfig = {
OnCalendar = cfg.interval;
};
wantedBy = [ "timers.target" ];
};
};
meta.maintainers = with maintainers; [ ];
}

View file

@ -0,0 +1,253 @@
{ pkgs, lib, config, ... }:
with lib;
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}
${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""}
${cfg.extraConfig.gammu}
[smsd]
LogFile = ${cfg.log.file}
Service = ${cfg.backend.service}
${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}
''}
${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "sqlite") ''
Driver = ${cfg.backend.sql.driver}
DBDir = ${cfg.backend.sql.database}
''}
${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") (
with cfg.backend; ''
Driver = ${sql.driver}
${if (sql.database!= null) then "Database = ${sql.database}" else ""}
${if (sql.host != null) then "Host = ${sql.host}" else ""}
${if (sql.user != null) then "User = ${sql.user}" else ""}
${if (sql.password != null) then "Password = ${sql.password}" else ""}
'')}
${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 = mkEnableOption "gammu-smsd daemon";
user = mkOption {
type = types.str;
default = "smsd";
description = "User that has access to the device";
};
device = {
path = mkOption {
type = types.path;
description = "Device node or address of the phone";
example = "/dev/ttyUSB2";
};
group = mkOption {
type = types.str;
default = "root";
description = "Owner group of the device";
example = "dialout";
};
connection = mkOption {
type = types.str;
default = "at";
description = "Protocol which will be used to talk to the phone";
};
synchronizeTime = mkOption {
type = types.bool;
default = true;
description = "Whether to set time from computer to the phone during starting connection";
};
pin = mkOption {
type = types.nullOr types.str;
default = null;
description = "PIN code for the simcard";
};
};
log = {
file = mkOption {
type = types.str;
default = "syslog";
description = "Path to file where information about communication will be stored";
};
format = mkOption {
type = types.enum [ "nothing" "text" "textall" "textalldate" "errors" "errorsdate" "binary" ];
default = "errors";
description = "Determines what will be logged to the LogFile";
};
};
extraConfig = {
gammu = mkOption {
type = types.lines;
default = "";
description = "Extra config lines to be added into [gammu] section";
};
smsd = mkOption {
type = types.lines;
default = "";
description = "Extra config lines to be added into [smsd] section";
};
};
backend = {
service = mkOption {
type = types.enum [ "null" "files" "sql" ];
default = "null";
description = "Service to use to store sms data.";
};
files = {
inboxPath = mkOption {
type = types.path;
default = "/var/spool/sms/inbox/";
description = "Where the received SMSes are stored";
};
outboxPath = mkOption {
type = types.path;
default = "/var/spool/sms/outbox/";
description = "Where SMSes to be sent should be placed";
};
sentSMSPath = mkOption {
type = types.path;
default = "/var/spool/sms/sent/";
description = "Where the transmitted SMSes are placed";
};
errorSMSPath = mkOption {
type = types.path;
default = "/var/spool/sms/error/";
description = "Where SMSes with error in transmission is placed";
};
};
sql = {
driver = mkOption {
type = types.enum [ "native_mysql" "native_pgsql" "odbc" "dbi" ];
description = "DB driver to use";
};
sqlDialect = mkOption {
type = types.nullOr types.str;
default = null;
description = "SQL dialect to use (odbc driver only)";
};
database = mkOption {
type = types.nullOr types.str;
default = null;
description = "Database name to store sms data";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "Database server address";
};
user = mkOption {
type = types.nullOr types.str;
default = null;
description = "User name used for connection to the database";
};
password = mkOption {
type = types.nullOr types.str;
default = null;
description = "User password used for connetion to the database";
};
};
};
};
};
config = mkIf cfg.enable {
users.users.${cfg.user} = {
description = "gammu-smsd user";
isSystemUser = true;
group = cfg.device.group;
};
environment.systemPackages = with cfg.backend; [ gammuPackage ]
++ optionals (service == "sql" && sql.driver == "sqlite") [ pkgs.sqlite ];
systemd.services.gammu-smsd = {
description = "gammu-smsd daemon";
wantedBy = [ "multi-user.target" ];
wants = with cfg.backend; [ ]
++ optionals (service == "sql" && sql.driver == "native_pgsql") [ "postgresql.service" ];
preStart = with cfg.backend;
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}
'')
+ optionalString (service == "sql" && sql.driver == "sqlite") ''
cat "${gammuPackage}/${initDBDir}/sqlite.sql" \
| ${pkgs.sqlite.bin}/bin/sqlite3 ${sql.database}
''
+ (let execPsql = extraArgs: concatStringsSep " " [
(optionalString (sql.password != null) "PGPASSWORD=${sql.password}")
"${config.services.postgresql.package}/bin/psql"
(optionalString (sql.host != null) "-h ${sql.host}")
(optionalString (sql.user != null) "-U ${sql.user}")
"$extraArgs"
"${sql.database}"
]; in 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,187 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.geoipupdate;
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
<productname>geoipupdate</productname>.
'';
interval = lib.mkOption {
type = lib.types.str;
default = "weekly";
description = ''
Update the GeoIP databases at this time / interval.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
settings = lib.mkOption {
description = ''
<productname>geoipupdate</productname> configuration
options. See
<link xlink:href="https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md" />
for a full list of available options.
'';
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 <literal>GeoIP2-City</literal> and old
numeric IDs like <literal>106</literal>.
'';
};
LicenseKey = lib.mkOption {
type = lib.types.path;
description = ''
A file containing the <productname>MaxMind</productname>
license key.
'';
};
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 <literal>geoip</literal> 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 = ''
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"
];
wants = [ "network-online.target" ];
startAt = cfg.interval;
serviceConfig = {
ExecStartPre =
let
geoipupdateKeyValue = lib.generators.toKeyValue {
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
mkValueString = v: with builtins;
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 throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
};
};
geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
script = ''
chown geoip "${cfg.settings.DatabaseDirectory}"
cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
${pkgs.replace-secret}/bin/replace-secret '${cfg.settings.LicenseKey}' \
'${cfg.settings.LicenseKey}' \
/run/geoipupdate/GeoIP.conf
'';
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" "~@resources" ];
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 ];
}

View file

@ -0,0 +1,658 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.gitea;
opt = options.services.gitea;
gitea = cfg.package;
pg = config.services.postgresql;
useMysql = cfg.database.type == "mysql";
usePostgresql = cfg.database.type == "postgres";
useSqlite = cfg.database.type == "sqlite3";
configFile = pkgs.writeText "app.ini" ''
APP_NAME = ${cfg.appName}
RUN_USER = ${cfg.user}
RUN_MODE = prod
${generators.toINI {} cfg.settings}
${optionalString (cfg.extraConfig != null) cfg.extraConfig}
'';
in
{
options = {
services.gitea = {
enable = mkOption {
default = false;
type = types.bool;
description = "Enable Gitea Service.";
};
package = mkOption {
default = pkgs.gitea;
type = types.package;
defaultText = literalExpression "pkgs.gitea";
description = "gitea derivation to use";
};
useWizard = mkOption {
default = false;
type = types.bool;
description = "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator.";
};
stateDir = mkOption {
default = "/var/lib/gitea";
type = types.str;
description = "gitea data directory.";
};
log = {
rootPath = 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.";
};
};
user = mkOption {
type = types.str;
default = "gitea";
description = "User account under which gitea 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 3306 else pg.port);
defaultText = literalExpression ''
if config.${opt.database.type} != "postgresql"
then 3306
else config.${options.services.postgresql.port}
'';
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "gitea";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "gitea";
description = "Database user.";
};
password = mkOption {
type = types.str;
default = "";
description = ''
The password corresponding to <option>database.user</option>.
Warning: this is stored in cleartext in the Nix store!
Use <option>database.passwordFile</option> instead.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/gitea-dbpassword";
description = ''
A file containing the password corresponding to
<option>database.user</option>.
'';
};
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/gitea.db";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.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 = mkOption {
type = types.bool;
default = false;
description = ''
Enable a timer that runs gitea dump to generate backup-files of the
current gitea database and repositories.
'';
};
interval = mkOption {
type = types.str;
default = "04:31";
example = "hourly";
description = ''
Run a gitea dump at this interval. Runs by default at 04:31 every day.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
backupDir = mkOption {
type = types.str;
default = "${cfg.stateDir}/dump";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
description = "Path to the dump files.";
};
type = mkOption {
type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ];
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 choosen by gitea.";
example = "gitea-dump";
};
};
ssh = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable external SSH feature.";
};
clonePort = mkOption {
type = types.int;
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.
'';
};
};
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.";
};
};
appName = mkOption {
type = types.str;
default = "gitea: Gitea Service";
description = "Application name.";
};
repositoryRoot = mkOption {
type = types.str;
default = "${cfg.stateDir}/repositories";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
description = "Path to the git repositories.";
};
domain = mkOption {
type = types.str;
default = "localhost";
description = "Domain name of your server.";
};
rootUrl = mkOption {
type = types.str;
default = "http://localhost:3000/";
description = "Full public URL of gitea server.";
};
httpAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = "HTTP listen address.";
};
httpPort = mkOption {
type = types.int;
default = 3000;
description = "HTTP listen port.";
};
enableUnixSocket = mkOption {
type = types.bool;
default = false;
description = "Configure Gitea to listen on a unix socket instead of the default TCP port.";
};
cookieSecure = 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 gitea is being served over HTTPS.
'';
};
staticRootPath = mkOption {
type = types.either types.str types.path;
default = gitea.data;
defaultText = literalExpression "package.data";
example = "/var/lib/gitea/data";
description = "Upper level of template and static files path.";
};
mailerPasswordFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/var/lib/secrets/gitea/mailpw";
description = "Path to a file containing the SMTP password.";
};
disableRegistration = mkEnableOption "the registration lock" // {
description = ''
By default any user can create an account on this <literal>gitea</literal> instance.
This can be disabled by using this option.
<emphasis>Note:</emphasis> please keep in mind that this should be added after the initial
deploy unless <link linkend="opt-services.gitea.useWizard">services.gitea.useWizard</link>
is <literal>true</literal> as the first registered user will be the administrator if
no install wizard is used.
'';
};
settings = mkOption {
type = with types; attrsOf (attrsOf (oneOf [ bool int str ]));
default = {};
description = ''
Gitea configuration. Refer to <link xlink:href="https://docs.gitea.io/en-us/config-cheat-sheet/"/>
for details on supported values.
'';
example = literalExpression ''
{
"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;
};
}
'';
};
extraConfig = mkOption {
type = with types; nullOr str;
default = null;
description = "Configuration lines appended to the generated gitea configuration file.";
};
};
};
config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user;
message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned";
}
];
services.gitea.settings = {
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;
PASSWD = "#dbpass#";
})
(mkIf useSqlite {
PATH = cfg.database.path;
})
(mkIf usePostgresql {
SSL_MODE = "disable";
})
];
repository = {
ROOT = cfg.repositoryRoot;
};
server = mkMerge [
{
DOMAIN = cfg.domain;
STATIC_ROOT_PATH = toString cfg.staticRootPath;
LFS_JWT_SECRET = "#lfsjwtsecret#";
ROOT_URL = cfg.rootUrl;
}
(mkIf cfg.enableUnixSocket {
PROTOCOL = "unix";
HTTP_ADDR = "/run/gitea/gitea.sock";
})
(mkIf (!cfg.enableUnixSocket) {
HTTP_ADDR = cfg.httpAddress;
HTTP_PORT = cfg.httpPort;
})
(mkIf cfg.ssh.enable {
DISABLE_SSH = false;
SSH_PORT = cfg.ssh.clonePort;
})
(mkIf (!cfg.ssh.enable) {
DISABLE_SSH = true;
})
(mkIf cfg.lfs.enable {
LFS_START_SERVER = true;
LFS_CONTENT_PATH = cfg.lfs.contentDir;
})
];
session = {
COOKIE_NAME = "session";
COOKIE_SECURE = cfg.cookieSecure;
};
security = {
SECRET_KEY = "#secretkey#";
INTERNAL_TOKEN = "#internaltoken#";
INSTALL_LOCK = true;
};
log = {
ROOT_PATH = cfg.log.rootPath;
LEVEL = cfg.log.level;
};
service = {
DISABLE_REGISTRATION = cfg.disableRegistration;
};
mailer = mkIf (cfg.mailerPasswordFile != null) {
PASSWD = "#mailerpass#";
};
oauth2 = {
JWT_SECRET = "#oauth2jwtsecret#";
};
};
services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
enable = mkDefault true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{ name = cfg.database.user;
ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
}
];
};
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} gitea - -"
"z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
"Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
"d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
"z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
"Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
"d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
"z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
"Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
"d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
"d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
"d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
"d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
"d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
"z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
"z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
"z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
"z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
"z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
"z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
"Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
# If we have a folder or symlink with gitea locales, remove it
# And symlink the current gitea locales in place
"L+ '${cfg.stateDir}/conf/locale' - - - - ${gitea.out}/locale"
];
systemd.services.gitea = {
description = "gitea";
after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
wantedBy = [ "multi-user.target" ];
path = [ gitea pkgs.git ];
# 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 persistant 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 = let
runConfig = "${cfg.stateDir}/custom/conf/app.ini";
secretKey = "${cfg.stateDir}/custom/conf/secret_key";
oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
internalToken = "${cfg.stateDir}/custom/conf/internal_token";
replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
in ''
# copy custom configuration and generate a random secret key if needed
${optionalString (cfg.useWizard == false) ''
function gitea_setup {
cp -f ${configFile} ${runConfig}
if [ ! -e ${secretKey} ]; then
${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
fi
# Migrate LFS_JWT_SECRET filename
if [[ -e ${oldLfsJwtSecret} && ! -e ${lfsJwtSecret} ]]; then
mv ${oldLfsJwtSecret} ${lfsJwtSecret}
fi
if [ ! -e ${oauth2JwtSecret} ]; then
${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
fi
if [ ! -e ${lfsJwtSecret} ]; then
${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
fi
if [ ! -e ${internalToken} ]; then
${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
fi
chmod u+w '${runConfig}'
${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}'
${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}'
${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}'
${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}'
${lib.optionalString (cfg.mailerPasswordFile != null) ''
${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}'
''}
chmod u-w '${runConfig}'
}
(umask 027; gitea_setup)
''}
# run migrations/init the database
${gitea}/bin/gitea migrate
# update all hooks' binary paths
${gitea}/bin/gitea admin regenerate hooks
# update command option in authorized_keys
if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
then
${gitea}/bin/gitea admin regenerate keys
fi
'';
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = "gitea";
WorkingDirectory = cfg.stateDir;
ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid";
Restart = "always";
# Runtime directory and mode
RuntimeDirectory = "gitea";
RuntimeDirectoryMode = "0755";
# Access write directories
ReadWritePaths = [ 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" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
# System Call Filtering
SystemCallArchitectures = "native";
SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
};
environment = {
USER = cfg.user;
HOME = cfg.stateDir;
GITEA_WORK_DIR = cfg.stateDir;
};
};
users.users = mkIf (cfg.user == "gitea") {
gitea = {
description = "Gitea Service";
home = cfg.stateDir;
useDefaultShell = true;
group = "gitea";
isSystemUser = true;
};
};
users.groups.gitea = {};
warnings =
optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
optional (cfg.extraConfig != null) ''
services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
'';
# Create database passwordFile default when password is configured.
services.gitea.database.passwordFile =
(mkDefault (toString (pkgs.writeTextFile {
name = "gitea-database-password";
text = cfg.database.password;
})));
systemd.services.gitea-dump = mkIf cfg.dump.enable {
description = "gitea dump";
after = [ "gitea.service" ];
wantedBy = [ "default.target" ];
path = [ gitea ];
environment = {
USER = cfg.user;
HOME = cfg.stateDir;
GITEA_WORK_DIR = cfg.stateDir;
};
serviceConfig = {
Type = "oneshot";
User = cfg.user;
ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
WorkingDirectory = cfg.dump.backupDir;
};
};
systemd.timers.gitea-dump = mkIf cfg.dump.enable {
description = "Update timer for gitea-dump";
partOf = [ "gitea-dump.service" ];
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = cfg.dump.interval;
};
};
meta.maintainers = with lib.maintainers; [ srhb ma27 ];
}

View file

@ -0,0 +1,725 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.gitit;
homeDir = "/var/lib/gitit";
toYesNo = b: if b then "yes" else "no";
gititShared = with cfg.haskellPackages; gitit + "/share/" + ghc.targetPrefix + ghc.haskellCompilerName + "/" + gitit.pname + "-" + gitit.version;
gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self));
gititSh = hsPkgs: extras: with pkgs; let
env = gititWithPkgs hsPkgs extras;
in writeScript "gitit" ''
#!${runtimeShell}
cd $HOME
export NIX_GHC="${env}/bin/ghc"
export NIX_GHCPKG="${env}/bin/ghc-pkg"
export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html"
export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir )
${env}/bin/gitit -f ${configFile}
'';
gititOptions = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable the gitit service.";
};
haskellPackages = mkOption {
default = pkgs.haskellPackages;
defaultText = literalExpression "pkgs.haskellPackages";
example = literalExpression "pkgs.haskell.packages.ghc784";
description = "haskellPackages used to build gitit and plugins.";
};
extraPackages = mkOption {
type = types.functionTo (types.listOf types.package);
default = self: [];
example = literalExpression ''
haskellPackages: [
haskellPackages.wreq
]
'';
description = ''
Extra packages available to ghc when running gitit. The
value must be a function which receives the attrset defined
in <varname>haskellPackages</varname> as the sole argument.
'';
};
address = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address on which the web server will listen.";
};
port = mkOption {
type = types.int;
default = 5001;
description = "Port on which the web server will run.";
};
wikiTitle = mkOption {
type = types.str;
default = "Gitit!";
description = "The wiki title.";
};
repositoryType = mkOption {
type = types.enum ["git" "darcs" "mercurial"];
default = "git";
description = "Specifies the type of repository used for wiki content.";
};
repositoryPath = mkOption {
type = types.path;
default = homeDir + "/wiki";
description = ''
Specifies the path of the repository directory. If it does not
exist, gitit will create it on startup.
'';
};
requireAuthentication = mkOption {
type = types.enum [ "none" "modify" "read" ];
default = "modify";
description = ''
If 'none', login is never required, and pages can be edited
anonymously. If 'modify', login is required to modify the wiki
(edit, add, delete pages, upload files). If 'read', login is
required to see any wiki pages.
'';
};
authenticationMethod = mkOption {
type = types.enum [ "form" "http" "generic" "github" ];
default = "form";
description = ''
'form' means that users will be logged in and registered using forms
in the gitit web interface. 'http' means that gitit will assume that
HTTP authentication is in place and take the logged in username from
the "Authorization" field of the HTTP request header (in addition,
the login/logout and registration links will be suppressed).
'generic' means that gitit will assume that some form of
authentication is in place that directly sets REMOTE_USER to the name
of the authenticated user (e.g. mod_auth_cas on apache). 'rpx' means
that gitit will attempt to log in through https://rpxnow.com. This
requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below,
and that 'curl' be in the system path.
'';
};
userFile = mkOption {
type = types.path;
default = homeDir + "/gitit-users";
description = ''
Specifies the path of the file containing user login information. If
it does not exist, gitit will create it (with an empty user list).
This file is not used if 'http' is selected for
authentication-method.
'';
};
sessionTimeout = mkOption {
type = types.int;
default = 60;
description = ''
Number of minutes of inactivity before a session expires.
'';
};
staticDir = mkOption {
type = types.path;
default = gititShared + "/data/static";
description = ''
Specifies the path of the static directory (containing javascript,
css, and images). If it does not exist, gitit will create it and
populate it with required scripts, stylesheets, and images.
'';
};
defaultPageType = mkOption {
type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ];
default = "markdown";
description = ''
Specifies the type of markup used to interpret pages in the wiki.
Possible values are markdown, rst, latex, html, markdown+lhs,
rst+lhs, and latex+lhs. (the +lhs variants treat the input as
literate Haskell. See pandoc's documentation for more details.) If
Markdown is selected, pandoc's syntax extensions (for footnotes,
delimited code blocks, etc.) will be enabled. Note that pandoc's
restructuredtext parser is not complete, so some pages may not be
rendered correctly if rst is selected. The same goes for latex and
html.
'';
};
math = mkOption {
type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ];
default = "mathml";
description = ''
Specifies how LaTeX math is to be displayed. Possible values are
mathml, raw, mathjax, jsmath, and google. If mathml is selected,
gitit will convert LaTeX math to MathML and link in a script,
MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers,
IE + mathplayer, and Opera. In other browsers you may get a jumble of
characters. If raw is selected, the LaTeX math will be displayed as
raw LaTeX math. If mathjax is selected, gitit will link to the
remote mathjax script. If jsMath is selected, gitit will link to the
script /js/jsMath/easy/load.js, and will assume that jsMath has been
installed into the js/jsMath directory. This is the most portable
solution. If google is selected, the google chart API is called to
render the formula as an image. This requires a connection to google,
and might raise a technical or a privacy problem.
'';
};
mathJaxScript = mkOption {
type = types.str;
default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
description = ''
Specifies the path to MathJax rendering script. You might want to
use your own MathJax script to render formulas without Internet
connection or if you want to use some special LaTeX packages. Note:
path specified there cannot be an absolute path to a script on your
hdd, instead you should run your (local if you wish) HTTP server
which will serve the MathJax.js script. You can easily (in four lines
of code) serve MathJax.js using
http://happstack.com/docs/crashcourse/FileServing.html Do not forget
the "http://" prefix (e.g. http://localhost:1234/MathJax.js).
'';
};
showLhsBirdTracks = mkOption {
type = types.bool;
default = false;
description = ''
Specifies whether to show Haskell code blocks in "bird style", with
"> " at the beginning of each line.
'';
};
templatesDir = mkOption {
type = types.path;
default = gititShared + "/data/templates";
description = ''
Specifies the path of the directory containing page templates. If it
does not exist, gitit will create it with default templates. Users
may wish to edit the templates to customize the appearance of their
wiki. The template files are HStringTemplate templates. Variables to
be interpolated appear between $\'s. Literal $\'s must be
backslash-escaped.
'';
};
logFile = mkOption {
type = types.path;
default = homeDir + "/gitit.log";
description = ''
Specifies the path of gitit's log file. If it does not exist, gitit
will create it. The log is in Apache combined log format.
'';
};
logLevel = mkOption {
type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ];
default = "ERROR";
description = ''
Determines how much information is logged. Possible values (from
most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR,
CRITICAL, ALERT, EMERGENCY.
'';
};
frontPage = mkOption {
type = types.str;
default = "Front Page";
description = ''
Specifies which wiki page is to be used as the wiki's front page.
Gitit creates a default front page on startup, if one does not exist
already.
'';
};
noDelete = mkOption {
type = types.str;
default = "Front Page, Help";
description = ''
Specifies pages that cannot be deleted through the web interface.
(They can still be deleted directly using git or darcs.) A
comma-separated list of page names. Leave blank to allow every page
to be deleted.
'';
};
noEdit = mkOption {
type = types.str;
default = "Help";
description = ''
Specifies pages that cannot be edited through the web interface.
Leave blank to allow every page to be edited.
'';
};
defaultSummary = mkOption {
type = types.str;
default = "";
description = ''
Specifies text to be used in the change description if the author
leaves the "description" field blank. If default-summary is blank
(the default), the author will be required to fill in the description
field.
'';
};
tableOfContents = mkOption {
type = types.bool;
default = true;
description = ''
Specifies whether to print a tables of contents (with links to
sections) on each wiki page.
'';
};
plugins = mkOption {
type = with types; listOf str;
default = [ (gititShared + "/plugins/Dot.hs") ];
description = ''
Specifies a list of plugins to load. Plugins may be specified either
by their path or by their module name. If the plugin name starts
with Gitit.Plugin., gitit will assume that the plugin is an installed
module and will not try to find a source file.
'';
};
useCache = mkOption {
type = types.bool;
default = false;
description = ''
Specifies whether to cache rendered pages. Note that if use-feed is
selected, feeds will be cached regardless of the value of use-cache.
'';
};
cacheDir = mkOption {
type = types.path;
default = homeDir + "/cache";
description = "Path where rendered pages will be cached.";
};
maxUploadSize = mkOption {
type = types.str;
default = "1000K";
description = ''
Specifies an upper limit on the size (in bytes) of files uploaded
through the wiki's web interface. To disable uploads, set this to
0K. This will result in the uploads link disappearing and the
_upload url becoming inactive.
'';
};
maxPageSize = mkOption {
type = types.str;
default = "1000K";
description = "Specifies an upper limit on the size (in bytes) of pages.";
};
debugMode = mkOption {
type = types.bool;
default = false;
description = "Causes debug information to be logged while gitit is running.";
};
compressResponses = mkOption {
type = types.bool;
default = true;
description = "Specifies whether HTTP responses should be compressed.";
};
mimeTypesFile = mkOption {
type = types.path;
default = "/etc/mime/types.info";
description = ''
Specifies the path of a file containing mime type mappings. Each
line of the file should contain two fields, separated by whitespace.
The first field is the mime type, the second is a file extension.
For example:
<programlisting>
video/x-ms-wmx wmx
</programlisting>
If the file is not found, some simple defaults will be used.
'';
};
useReCaptcha = mkOption {
type = types.bool;
default = false;
description = ''
If true, causes gitit to use the reCAPTCHA service
(http://recaptcha.net) to prevent bots from creating accounts.
'';
};
reCaptchaPrivateKey = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Specifies the private key for the reCAPTCHA service. To get
these, you need to create an account at http://recaptcha.net.
'';
};
reCaptchaPublicKey = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Specifies the public key for the reCAPTCHA service. To get
these, you need to create an account at http://recaptcha.net.
'';
};
accessQuestion = mkOption {
type = types.str;
default = "What is the code given to you by Ms. X?";
description = ''
Specifies a question that users must answer when they attempt to
create an account
'';
};
accessQuestionAnswers = mkOption {
type = types.str;
default = "RED DOG, red dog";
description = ''
Specifies a question that users must answer when they attempt to
create an account, along with a comma-separated list of acceptable
answers. This can be used to institute a rudimentary password for
signing up as a user on the wiki, or as an alternative to reCAPTCHA.
Example:
access-question: What is the code given to you by Ms. X?
access-question-answers: RED DOG, red dog
'';
};
rpxDomain = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Specifies the domain and key of your RPX account. The domain is just
the prefix of the complete RPX domain, so if your full domain is
'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain.
'';
};
rpxKey = mkOption {
type = with types; nullOr str;
default = null;
description = "RPX account access key.";
};
mailCommand = mkOption {
type = types.str;
default = "sendmail %s";
description = ''
Specifies the command to use to send notification emails. '%s' will
be replaced by the destination email address. The body of the
message will be read from stdin. If this field is left blank,
password reset will not be offered.
'';
};
resetPasswordMessage = mkOption {
type = types.lines;
default = ''
> From: gitit@$hostname$
> To: $useremail$
> Subject: Wiki password reset
>
> Hello $username$,
>
> To reset your password, please follow the link below:
> http://$hostname$:$port$$resetlink$
>
> Regards
'';
description = ''
Gives the text of the message that will be sent to the user should
she want to reset her password, or change other registration info.
The lines must be indented, and must begin with '>'. The initial
spaces and '> ' will be stripped off. $username$ will be replaced by
the user's username, $useremail$ by her email address, $hostname$ by
the hostname on which the wiki is running (as returned by the
hostname system call), $port$ by the port on which the wiki is
running, and $resetlink$ by the relative path of a reset link derived
from the user's existing hashed password. If your gitit wiki is being
proxied to a location other than the root path of $port$, you should
change the link to reflect this: for example, to
http://$hostname$/path/to/wiki$resetlink$ or
http://gitit.$hostname$$resetlink$
'';
};
useFeed = mkOption {
type = types.bool;
default = false;
description = ''
Specifies whether an ATOM feed should be enabled (for the site and
for individual pages).
'';
};
baseUrl = mkOption {
type = with types; nullOr str;
default = null;
description = ''
The base URL of the wiki, to be used in constructing feed IDs and RPX
token_urls. Set this if useFeed is false or authentication-method
is 'rpx'.
'';
};
absoluteUrls = mkOption {
type = types.bool;
default = false;
description = ''
Make wikilinks absolute with respect to the base-url. So, for
example, in a wiki served at the base URL '/wiki', on a page
Sub/Page, the wikilink '[Cactus]()' will produce a link to
'/wiki/Cactus' if absoluteUrls is true, and a relative link to
'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'.
'';
};
feedDays = mkOption {
type = types.int;
default = 14;
description = "Number of days to be included in feeds.";
};
feedRefreshTime = mkOption {
type = types.int;
default = 60;
description = "Number of minutes to cache feeds before refreshing.";
};
pdfExport = mkOption {
type = types.bool;
default = false;
description = ''
If true, PDF will appear in export options. PDF will be created using
pdflatex, which must be installed and in the path. Note that PDF
exports create significant additional server load.
'';
};
pandocUserData = mkOption {
type = with types; nullOr path;
default = null;
description = ''
If a directory is specified, this will be searched for pandoc
customizations. These can include a templates/ directory for custom
templates for various export formats, an S5 directory for custom S5
styles, and a reference.odt for ODT exports. If no directory is
specified, $HOME/.pandoc will be searched. See pandoc's README for
more information.
'';
};
xssSanitize = mkOption {
type = types.bool;
default = true;
description = ''
If true, all HTML (including that produced by pandoc) is filtered
through xss-sanitize. Set to no only if you trust all of your users.
'';
};
oauthClientId = mkOption {
type = with types; nullOr str;
default = null;
description = "OAuth client ID";
};
oauthClientSecret = mkOption {
type = with types; nullOr str;
default = null;
description = "OAuth client secret";
};
oauthCallback = mkOption {
type = with types; nullOr str;
default = null;
description = "OAuth callback URL";
};
oauthAuthorizeEndpoint = mkOption {
type = with types; nullOr str;
default = null;
description = "OAuth authorize endpoint";
};
oauthAccessTokenEndpoint = mkOption {
type = with types; nullOr str;
default = null;
description = "OAuth access token endpoint";
};
githubOrg = mkOption {
type = with types; nullOr str;
default = null;
description = "Github organization";
};
};
configFile = pkgs.writeText "gitit.conf" ''
address: ${cfg.address}
port: ${toString cfg.port}
wiki-title: ${cfg.wikiTitle}
repository-type: ${cfg.repositoryType}
repository-path: ${cfg.repositoryPath}
require-authentication: ${cfg.requireAuthentication}
authentication-method: ${cfg.authenticationMethod}
user-file: ${cfg.userFile}
session-timeout: ${toString cfg.sessionTimeout}
static-dir: ${cfg.staticDir}
default-page-type: ${cfg.defaultPageType}
math: ${cfg.math}
mathjax-script: ${cfg.mathJaxScript}
show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks}
templates-dir: ${cfg.templatesDir}
log-file: ${cfg.logFile}
log-level: ${cfg.logLevel}
front-page: ${cfg.frontPage}
no-delete: ${cfg.noDelete}
no-edit: ${cfg.noEdit}
default-summary: ${cfg.defaultSummary}
table-of-contents: ${toYesNo cfg.tableOfContents}
plugins: ${concatStringsSep "," cfg.plugins}
use-cache: ${toYesNo cfg.useCache}
cache-dir: ${cfg.cacheDir}
max-upload-size: ${cfg.maxUploadSize}
max-page-size: ${cfg.maxPageSize}
debug-mode: ${toYesNo cfg.debugMode}
compress-responses: ${toYesNo cfg.compressResponses}
mime-types-file: ${cfg.mimeTypesFile}
use-recaptcha: ${toYesNo cfg.useReCaptcha}
recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey}
recaptcha-public-key: ${toString cfg.reCaptchaPublicKey}
access-question: ${cfg.accessQuestion}
access-question-answers: ${cfg.accessQuestionAnswers}
rpx-domain: ${toString cfg.rpxDomain}
rpx-key: ${toString cfg.rpxKey}
mail-command: ${cfg.mailCommand}
reset-password-message: ${cfg.resetPasswordMessage}
use-feed: ${toYesNo cfg.useFeed}
base-url: ${toString cfg.baseUrl}
absolute-urls: ${toYesNo cfg.absoluteUrls}
feed-days: ${toString cfg.feedDays}
feed-refresh-time: ${toString cfg.feedRefreshTime}
pdf-export: ${toYesNo cfg.pdfExport}
pandoc-user-data: ${toString cfg.pandocUserData}
xss-sanitize: ${toYesNo cfg.xssSanitize}
[Github]
oauthclientid: ${toString cfg.oauthClientId}
oauthclientsecret: ${toString cfg.oauthClientSecret}
oauthcallback: ${toString cfg.oauthCallback}
oauthauthorizeendpoint: ${toString cfg.oauthAuthorizeEndpoint}
oauthaccesstokenendpoint: ${toString cfg.oauthAccessTokenEndpoint}
github-org: ${toString cfg.githubOrg}
'';
in
{
options.services.gitit = gititOptions;
config = mkIf cfg.enable {
users.users.gitit = {
group = config.users.groups.gitit.name;
description = "Gitit user";
home = homeDir;
createHome = true;
uid = config.ids.uids.gitit;
};
users.groups.gitit.gid = config.ids.gids.gitit;
systemd.services.gitit = let
uid = toString config.ids.uids.gitit;
gid = toString config.ids.gids.gitit;
in {
description = "Git and Pandoc Powered Wiki";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ curl ]
++ optional cfg.pdfExport texlive.combined.scheme-basic
++ optional (cfg.repositoryType == "darcs") darcs
++ optional (cfg.repositoryType == "mercurial") mercurial
++ optional (cfg.repositoryType == "git") git;
preStart = let
gm = "gitit@${config.networking.hostName}";
in
with cfg; ''
chown ${uid}:${gid} -R ${homeDir}
for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir}
do
if [ ! -d $dir ]
then
mkdir -p $dir
find $dir -type d -exec chmod 0750 {} +
find $dir -type f -exec chmod 0640 {} +
fi
done
cd ${repositoryPath}
${
if repositoryType == "darcs" then
''
if [ ! -d _darcs ]
then
${pkgs.darcs}/bin/darcs initialize
echo "${gm}" > _darcs/prefs/email
''
else if repositoryType == "mercurial" then
''
if [ ! -d .hg ]
then
${pkgs.mercurial}/bin/hg init
cat >> .hg/hgrc <<NAMED
[ui]
username = gitit ${gm}
NAMED
''
else
''
if [ ! -d .git ]
then
${pkgs.git}/bin/git init
${pkgs.git}/bin/git config user.email "${gm}"
${pkgs.git}/bin/git config user.name "gitit"
''}
chown ${uid}:${gid} -R ${repositoryPath}
fi
cd -
'';
serviceConfig = {
User = config.users.users.gitit.name;
Group = config.users.groups.gitit.name;
ExecStart = with cfg; gititSh haskellPackages extraPackages;
};
};
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,151 @@
<chapter xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0"
xml:id="module-services-gitlab">
<title>GitLab</title>
<para>
GitLab is a feature-rich git hosting service.
</para>
<section xml:id="module-services-gitlab-prerequisites">
<title>Prerequisites</title>
<para>
The <literal>gitlab</literal> service exposes only an Unix socket at
<literal>/run/gitlab/gitlab-workhorse.socket</literal>. You need to
configure a webserver to proxy HTTP requests to the socket.
</para>
<para>
For instance, the following configuration could be used to use nginx as
frontend proxy:
<programlisting>
<link linkend="opt-services.nginx.enable">services.nginx</link> = {
<link linkend="opt-services.nginx.enable">enable</link> = true;
<link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
<link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
<link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
<link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link>."git.example.com" = {
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
<link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
<link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">locations."/".proxyPass</link> = "http://unix:/run/gitlab/gitlab-workhorse.socket";
};
};
</programlisting>
</para>
</section>
<section xml:id="module-services-gitlab-configuring">
<title>Configuring</title>
<para>
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.
</para>
<para>
The default state dir is <literal>/var/gitlab/state</literal>. This is where
all data like the repositories and uploads will be stored.
</para>
<para>
A basic configuration with some custom settings could look like this:
<programlisting>
services.gitlab = {
<link linkend="opt-services.gitlab.enable">enable</link> = true;
<link linkend="opt-services.gitlab.databasePasswordFile">databasePasswordFile</link> = "/var/keys/gitlab/db_password";
<link linkend="opt-services.gitlab.initialRootPasswordFile">initialRootPasswordFile</link> = "/var/keys/gitlab/root_password";
<link linkend="opt-services.gitlab.https">https</link> = true;
<link linkend="opt-services.gitlab.host">host</link> = "git.example.com";
<link linkend="opt-services.gitlab.port">port</link> = 443;
<link linkend="opt-services.gitlab.user">user</link> = "git";
<link linkend="opt-services.gitlab.group">group</link> = "git";
smtp = {
<link linkend="opt-services.gitlab.smtp.enable">enable</link> = true;
<link linkend="opt-services.gitlab.smtp.address">address</link> = "localhost";
<link linkend="opt-services.gitlab.smtp.port">port</link> = 25;
};
secrets = {
<link linkend="opt-services.gitlab.secrets.dbFile">dbFile</link> = "/var/keys/gitlab/db";
<link linkend="opt-services.gitlab.secrets.secretFile">secretFile</link> = "/var/keys/gitlab/secret";
<link linkend="opt-services.gitlab.secrets.otpFile">otpFile</link> = "/var/keys/gitlab/otp";
<link linkend="opt-services.gitlab.secrets.jwsFile">jwsFile</link> = "/var/keys/gitlab/jws";
};
<link linkend="opt-services.gitlab.extraConfig">extraConfig</link> = {
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; };
};
};
};
</programlisting>
</para>
<para>
If you're setting up a new GitLab instance, generate new
secrets. You for instance use <literal>tr -dc A-Za-z0-9 &lt;
/dev/urandom | head -c 128 &gt; /var/keys/gitlab/db</literal> to
generate a new db secret. Make sure the files can be read by, and
only by, the user specified by <link
linkend="opt-services.gitlab.user">services.gitlab.user</link>. GitLab
encrypts sensitive data stored in the database. If you're restoring
an existing GitLab instance, you must specify the secrets secret
from <literal>config/secrets.yml</literal> located in your GitLab
state folder.
</para>
<para>
When <literal>incoming_mail.enabled</literal> is set to <literal>true</literal>
in <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> an additional
service called <literal>gitlab-mailroom</literal> is enabled for fetching incoming mail.
</para>
<para>
Refer to <xref linkend="ch-options" /> for all available configuration
options for the
<link linkend="opt-services.gitlab.enable">services.gitlab</link> module.
</para>
</section>
<section xml:id="module-services-gitlab-maintenance">
<title>Maintenance</title>
<section xml:id="module-services-gitlab-maintenance-backups">
<title>Backups</title>
<para>
Backups can be configured with the options in <link
linkend="opt-services.gitlab.backup.keepTime">services.gitlab.backup</link>. Use
the <link
linkend="opt-services.gitlab.backup.startAt">services.gitlab.backup.startAt</link>
option to configure regular backups.
</para>
<para>
To run a manual backup, start the <literal>gitlab-backup</literal> service:
<screen>
<prompt>$ </prompt>systemctl start gitlab-backup.service
</screen>
</para>
</section>
<section xml:id="module-services-gitlab-maintenance-rake">
<title>Rake tasks</title>
<para>
You can run GitLab's rake tasks with <literal>gitlab-rake</literal>
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.
</para>
<para>
A list of all availabe rake tasks can be obtained by running:
<screen>
<prompt>$ </prompt>sudo -u git -H gitlab-rake -T
</screen>
</para>
</section>
</section>
</chapter>

View file

@ -0,0 +1,234 @@
{ config, lib, pkgs, ... }:
with lib;
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 = mkOption {
type = types.bool;
default = false;
description = ''
Enable gitolite management under the
<literal>gitolite</literal> user. After
switching to a configuration with Gitolite enabled, you can
then run <literal>git clone
gitolite@host:gitolite-admin.git</literal> to manage it further.
'';
};
dataDir = mkOption {
type = 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 = mkOption {
type = 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 = mkOption {
type = types.bool;
default = false;
description = ''
Enable git-annex support. Uses the <literal>extraGitoliteRc</literal> option
to apply the necessary configuration.
'';
};
commonHooks = mkOption {
type = types.listOf types.path;
default = [];
description = ''
A list of custom git hooks that get copied to <literal>~/.gitolite/hooks/common</literal>.
'';
};
extraGitoliteRc = mkOption {
type = types.lines;
default = "";
example = 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 <literal>~/.gitolite.rc</literal>.
This should be Perl code that modifies the <literal>%RC</literal>
configuration variable. The default <literal>~/.gitolite.rc</literal>
content is generated by invoking <literal>gitolite print-default-rc</literal>,
and extra configuration from this option is appended to it. The result
is placed to Nix store, and the <literal>~/.gitolite.rc</literal> file
becomes a symlink to it.
If you already have a customized (or otherwise changed)
<literal>~/.gitolite.rc</literal> 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
<literal>~/.gitolite.rc</literal>, convert them to appropriate Perl
statements, add them to this option, and remove the file.
See also the <literal>enableGitAnnex</literal> option.
'';
};
user = mkOption {
type = types.str;
default = "gitolite";
description = ''
Gitolite user account. This is the username of the gitolite endpoint.
'';
};
group = mkOption {
type = types.str;
default = "gitolite";
description = ''
Primary group of the Gitolite user account.
'';
};
};
};
config = 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"
'' +
optionalString (cfg.extraGitoliteRc != "") ''
echo -n ${escapeShellArg ''
# Added by NixOS:
${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 = optionalString cfg.enableGitAnnex ''
# Enable git-annex support:
push( @{$RC{ENABLE}}, 'git-annex-shell ua');
'';
users.users.${cfg.user} = {
description = "Gitolite user";
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 = mkMerge [
(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 ]
++ optional cfg.enableGitAnnex pkgs.git-annex;
});
}

View file

@ -0,0 +1,60 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.gitweb;
in
{
options.services.gitweb = {
projectroot = mkOption {
default = "/srv/git";
type = types.path;
description = ''
Path to git projects (bare repositories) that should be served by
gitweb. Must not end with a slash.
'';
};
extraConfig = mkOption {
default = "";
type = 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 = mkOption {
default = false;
type = types.bool;
description = ''
Use an alternative theme for gitweb, strongly inspired by GitHub.
'';
};
gitwebConfigFile = mkOption {
default = pkgs.writeText "gitweb.conf" ''
# path to git projects (<project>.git)
$projectroot = "${cfg.projectroot}";
$highlight_bin = "${pkgs.highlight}/bin/highlight";
${cfg.extraConfig}
'';
defaultText = literalDocBook "generated config file";
type = types.path;
readOnly = true;
internal = true;
};
};
meta.maintainers = with maintainers; [ ];
}

View file

@ -0,0 +1,274 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.gogs;
opt = options.services.gogs;
configFile = pkgs.writeText "app.ini" ''
APP_NAME = ${cfg.appName}
RUN_USER = ${cfg.user}
RUN_MODE = prod
[database]
DB_TYPE = ${cfg.database.type}
HOST = ${cfg.database.host}:${toString cfg.database.port}
NAME = ${cfg.database.name}
USER = ${cfg.database.user}
PASSWD = #dbpass#
PATH = ${cfg.database.path}
[repository]
ROOT = ${cfg.repositoryRoot}
[server]
DOMAIN = ${cfg.domain}
HTTP_ADDR = ${cfg.httpAddress}
HTTP_PORT = ${toString cfg.httpPort}
ROOT_URL = ${cfg.rootUrl}
[session]
COOKIE_NAME = session
COOKIE_SECURE = ${boolToString cfg.cookieSecure}
[security]
SECRET_KEY = #secretkey#
INSTALL_LOCK = true
[log]
ROOT_PATH = ${cfg.stateDir}/log
${cfg.extraConfig}
'';
in
{
options = {
services.gogs = {
enable = mkOption {
default = false;
type = types.bool;
description = "Enable Go Git Service.";
};
useWizard = mkOption {
default = false;
type = types.bool;
description = "Do not generate a configuration and use Gogs' installation wizard instead. The first registered user will be administrator.";
};
stateDir = mkOption {
default = "/var/lib/gogs";
type = types.str;
description = "Gogs data directory.";
};
user = mkOption {
type = types.str;
default = "gogs";
description = "User account under which Gogs runs.";
};
group = mkOption {
type = types.str;
default = "gogs";
description = "Group account under which Gogs 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.int;
default = 3306;
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "gogs";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "gogs";
description = "Database user.";
};
password = mkOption {
type = types.str;
default = "";
description = ''
The password corresponding to <option>database.user</option>.
Warning: this is stored in cleartext in the Nix store!
Use <option>database.passwordFile</option> instead.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/gogs-dbpassword";
description = ''
A file containing the password corresponding to
<option>database.user</option>.
'';
};
path = mkOption {
type = types.str;
default = "${cfg.stateDir}/data/gogs.db";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gogs.db"'';
description = "Path to the sqlite3 database file.";
};
};
appName = mkOption {
type = types.str;
default = "Gogs: Go Git Service";
description = "Application name.";
};
repositoryRoot = mkOption {
type = types.str;
default = "${cfg.stateDir}/repositories";
defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
description = "Path to the git repositories.";
};
domain = mkOption {
type = types.str;
default = "localhost";
description = "Domain name of your server.";
};
rootUrl = mkOption {
type = types.str;
default = "http://localhost:3000/";
description = "Full public URL of Gogs server.";
};
httpAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = "HTTP listen address.";
};
httpPort = mkOption {
type = types.int;
default = 3000;
description = "HTTP listen port.";
};
cookieSecure = 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 Gogs is being served over HTTPS.
'';
};
extraConfig = mkOption {
type = types.str;
default = "";
description = "Configuration lines appended to the generated Gogs configuration file.";
};
};
};
config = mkIf cfg.enable {
systemd.services.gogs = {
description = "Gogs (Go Git Service)";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.gogs ];
preStart = let
runConfig = "${cfg.stateDir}/custom/conf/app.ini";
secretKey = "${cfg.stateDir}/custom/conf/secret_key";
in ''
mkdir -p ${cfg.stateDir}
# copy custom configuration and generate a random secret key if needed
${optionalString (cfg.useWizard == false) ''
mkdir -p ${cfg.stateDir}/custom/conf
cp -f ${configFile} ${runConfig}
if [ ! -e ${secretKey} ]; then
head -c 16 /dev/urandom | base64 > ${secretKey}
fi
KEY=$(head -n1 ${secretKey})
DBPASS=$(head -n1 ${cfg.database.passwordFile})
sed -e "s,#secretkey#,$KEY,g" \
-e "s,#dbpass#,$DBPASS,g" \
-i ${runConfig}
chmod 440 ${runConfig} ${secretKey}
''}
mkdir -p ${cfg.repositoryRoot}
# update all hooks' binary paths
HOOKS=$(find ${cfg.repositoryRoot} -mindepth 4 -maxdepth 4 -type f -wholename "*git/hooks/*")
if [ "$HOOKS" ]
then
sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gogs,${pkgs.gogs}/bin/gogs,g' $HOOKS
sed -ri 's,/nix/store/[a-z0-9.-]+/bin/env,${pkgs.coreutils}/bin/env,g' $HOOKS
sed -ri 's,/nix/store/[a-z0-9.-]+/bin/bash,${pkgs.bash}/bin/bash,g' $HOOKS
sed -ri 's,/nix/store/[a-z0-9.-]+/bin/perl,${pkgs.perl}/bin/perl,g' $HOOKS
fi
'';
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.stateDir;
ExecStart = "${pkgs.gogs}/bin/gogs web";
Restart = "always";
};
environment = {
USER = cfg.user;
HOME = cfg.stateDir;
GOGS_WORK_DIR = cfg.stateDir;
};
};
users = mkIf (cfg.user == "gogs") {
users.gogs = {
description = "Go Git Service";
uid = config.ids.uids.gogs;
group = "gogs";
home = cfg.stateDir;
createHome = true;
shell = pkgs.bash;
};
groups.gogs.gid = config.ids.gids.gogs;
};
warnings = optional (cfg.database.password != "")
''config.services.gogs.database.password will be stored as plaintext
in the Nix store. Use database.passwordFile instead.'';
# Create database passwordFile default when password is configured.
services.gogs.database.passwordFile =
(mkDefault (toString (pkgs.writeTextFile {
name = "gogs-database-password";
text = cfg.database.password;
})));
};
}

View file

@ -0,0 +1,135 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.gollum;
in
{
options.services.gollum = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable the Gollum service.";
};
address = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address on which the web server will listen.";
};
port = mkOption {
type = types.int;
default = 4567;
description = "Port on which the web server will run.";
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Content of the configuration file";
};
mathjax = mkOption {
type = types.bool;
default = false;
description = "Enable support for math rendering using MathJax";
};
allowUploads = mkOption {
type = types.nullOr (types.enum [ "dir" "page" ]);
default = null;
description = "Enable uploads of external files";
};
user-icons = mkOption {
type = types.nullOr (types.enum [ "gravatar" "identicon" ]);
default = null;
description = "User icons for history view";
};
emoji = mkOption {
type = types.bool;
default = false;
description = "Parse and interpret emoji tags";
};
h1-title = mkOption {
type = types.bool;
default = false;
description = "Use the first h1 as page title";
};
no-edit = mkOption {
type = types.bool;
default = false;
description = "Disable editing pages";
};
branch = mkOption {
type = types.str;
default = "master";
example = "develop";
description = "Git branch to serve";
};
stateDir = mkOption {
type = 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.";
};
};
config = mkIf cfg.enable {
users.users.gollum = {
group = config.users.users.gollum.name;
description = "Gollum user";
createHome = false;
isSystemUser = true;
};
users.groups.gollum = { };
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - ${config.users.users.gollum.name} ${config.users.groups.gollum.name} - -"
];
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 = config.users.users.gollum.name;
Group = config.users.groups.gollum.name;
WorkingDirectory = cfg.stateDir;
ExecStart = ''
${pkgs.gollum}/bin/gollum \
--port ${toString cfg.port} \
--host ${cfg.address} \
--config ${pkgs.writeText "gollum-config.rb" cfg.extraConfig} \
--ref ${cfg.branch} \
${optionalString cfg.mathjax "--mathjax"} \
${optionalString cfg.emoji "--emoji"} \
${optionalString cfg.h1-title "--h1-title"} \
${optionalString cfg.no-edit "--no-edit"} \
${optionalString (cfg.allowUploads != null) "--allow-uploads ${cfg.allowUploads}"} \
${optionalString (cfg.user-icons != null) "--user-icons ${cfg.user-icons}"} \
${cfg.stateDir}
'';
};
};
};
meta.maintainers = with lib.maintainers; [ erictapen bbenno ];
}

View file

@ -0,0 +1,116 @@
{ config, lib, pkgs, ... }:
with lib;
let
uid = config.ids.uids.gpsd;
gid = config.ids.gids.gpsd;
cfg = config.services.gpsd;
in
{
###### interface
options = {
services.gpsd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable `gpsd', a GPS service daemon.
'';
};
device = mkOption {
type = types.str;
default = "/dev/ttyUSB0";
description = ''
A device may be a local serial device for GPS input, or a URL of the form:
<literal>[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]</literal>
in which case it specifies an input source for DGPS or ntrip data.
'';
};
readonly = mkOption {
type = 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 = mkOption {
type = types.bool;
default = false;
description = ''
don't wait for client connects to poll GPS
'';
};
port = mkOption {
type = types.port;
default = 2947;
description = ''
The port where to listen for TCP connections.
'';
};
debugLevel = mkOption {
type = types.int;
default = 0;
description = ''
The debugging level.
'';
};
};
};
###### implementation
config = 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 = ''
${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}" \
-S "${toString cfg.port}" \
${optionalString cfg.readonly "-b"} \
${optionalString cfg.nowait "-n"} \
"${cfg.device}"
'';
};
};
};
}

View file

@ -0,0 +1,31 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.greenclip;
in {
options.services.greenclip = {
enable = mkEnableOption "Greenclip daemon";
package = mkOption {
type = types.package;
default = pkgs.haskellPackages.greenclip;
defaultText = literalExpression "pkgs.haskellPackages.greenclip";
description = "greenclip derivation to use.";
};
};
config = 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";
};
environment.systemPackages = [ cfg.package ];
};
}

View file

@ -0,0 +1,89 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
name = "headphones";
cfg = config.services.headphones;
opt = options.services.headphones;
in
{
###### interface
options = {
services.headphones = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the headphones server.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/${name}";
description = "Path where to store data files.";
};
configFile = mkOption {
type = types.path;
default = "${cfg.dataDir}/config.ini";
defaultText = literalExpression ''"''${config.${opt.dataDir}}/config.ini"'';
description = "Path to config file.";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "Host to listen on.";
};
port = mkOption {
type = types.ints.u16;
default = 8181;
description = "Port to bind to.";
};
user = mkOption {
type = types.str;
default = name;
description = "User to run the service as";
};
group = mkOption {
type = types.str;
default = name;
description = "Group to run the service as";
};
};
};
###### implementation
config = mkIf cfg.enable {
users.users = optionalAttrs (cfg.user == name) {
${name} = {
uid = config.ids.uids.headphones;
group = cfg.group;
description = "headphones user";
home = cfg.dataDir;
createHome = true;
};
};
users.groups = 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,222 @@
{ config, pkgs, lib, ... }:
with 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 = mkEnableOption "the Matrix to IRC bridge";
package = mkOption {
type = types.package;
default = pkgs.heisenbridge;
defaultText = "pkgs.heisenbridge";
example = "pkgs.heisenbridge.override { = ; }";
description = ''
Package of the application to run, exposed for overriding purposes.
'';
};
homeserver = mkOption {
type = types.str;
description = "The URL to the home server for client-server API calls";
example = "http://localhost:8008";
};
registrationUrl = mkOption {
type = 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 = mkOption {
type = 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 = mkOption {
type = types.port;
description = "The port to listen on";
default = 9898;
};
debug = mkOption {
type = types.bool;
description = "More verbose logging. Recommended during initial setup.";
default = false;
};
owner = mkOption {
type = types.nullOr types.str;
description = ''
Set owner MXID otherwise first talking local user will claim the bridge
'';
default = null;
example = "@admin:example.org";
};
namespaces = 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 = types.submodule {
freeformType = jsonType;
};
default = {
users = [
{
regex = "@irc_.*";
exclusive = true;
}
];
aliases = [ ];
rooms = [ ];
};
};
identd.enable = mkEnableOption "identd service support";
identd.port = mkOption {
type = types.port;
description = "identd listen port";
default = 113;
};
extraArgs = mkOption {
type = types.listOf types.str;
description = "Heisenbridge is configured over the command line. Append extra arguments here";
default = [ ];
};
};
config = 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 ${getBin pkgs.glibc}/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" ] ++ 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 = [ lib.maintainers.piegames ];
}

View file

@ -0,0 +1,65 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.services.ihaskell;
ihaskell = pkgs.ihaskell.override {
packages = cfg.extraPackages;
};
in
{
options = {
services.ihaskell = {
enable = mkOption {
type = types.bool;
default = false;
description = "Autostart an IHaskell notebook service.";
};
extraPackages = mkOption {
type = types.functionTo (types.listOf types.package);
default = haskellPackages: [];
defaultText = literalExpression "haskellPackages: []";
example = 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 <varname>haskellPackages</varname> as the sole argument.
'';
};
};
};
config = 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,30 @@
{ pkgs, lib, config, ... }:
with lib;
let cfg = config.services.input-remapper; in
{
options = {
services.input-remapper = {
enable = mkEnableOption "input-remapper, an easy to use tool to change the mapping of your input device buttons.";
package = options.mkPackageOption pkgs "input-remapper" { };
enableUdevRules = 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 = mkOption {
default = [ "graphical.target" ];
example = [ "multi-user.target" ];
type = types.listOf types.str;
description = "Specifies the WantedBy setting for the input-remapper service.";
};
};
};
config = mkIf cfg.enable {
services.udev.packages = 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,67 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.irkerd;
ports = [ 6659 ];
in
{
options.services.irkerd = {
enable = mkOption {
description = "Whether to enable irker, an IRC notification daemon.";
default = false;
type = types.bool;
};
openPorts = mkOption {
description = "Open ports in the firewall for irkerd";
default = false;
type = types.bool;
};
listenAddress = mkOption {
default = "localhost";
example = "0.0.0.0";
type = 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 = mkOption {
default = "irker";
type = types.str;
description = "Nick to use for irker";
};
};
config = 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 = mkIf cfg.openPorts ports;
networking.firewall.allowedUDPPorts = mkIf cfg.openPorts ports;
};
}

View file

@ -0,0 +1,82 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.jackett;
in
{
options = {
services.jackett = {
enable = mkEnableOption "Jackett";
dataDir = mkOption {
type = types.str;
default = "/var/lib/jackett/.config/Jackett";
description = "The directory where Jackett stores its data files.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the Jackett web interface.";
};
user = mkOption {
type = types.str;
default = "jackett";
description = "User account under which Jackett runs.";
};
group = mkOption {
type = types.str;
default = "jackett";
description = "Group under which Jackett runs.";
};
package = mkOption {
type = types.package;
default = pkgs.jackett;
defaultText = literalExpression "pkgs.jackett";
description = "Jackett package to use.";
};
};
};
config = 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 --DataFolder '${cfg.dataDir}'";
Restart = "on-failure";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 9117 ];
};
users.users = mkIf (cfg.user == "jackett") {
jackett = {
group = cfg.group;
home = cfg.dataDir;
uid = config.ids.uids.jackett;
};
};
users.groups = mkIf (cfg.group == "jackett") {
jackett.gid = config.ids.gids.jackett;
};
};
}

View file

@ -0,0 +1,125 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.jellyfin;
in
{
options = {
services.jellyfin = {
enable = mkEnableOption "Jellyfin Media Server";
user = mkOption {
type = types.str;
default = "jellyfin";
description = "User account under which Jellyfin runs.";
};
package = mkOption {
type = types.package;
default = pkgs.jellyfin;
defaultText = literalExpression "pkgs.jellyfin";
description = ''
Jellyfin package to use.
'';
};
group = mkOption {
type = types.str;
default = "jellyfin";
description = "Group under which jellyfin runs.";
};
openFirewall = mkOption {
type = types.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.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.jellyfin = {
description = "Jellyfin Media Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = rec {
User = cfg.user;
Group = cfg.group;
StateDirectory = "jellyfin";
StateDirectoryMode = "0700";
CacheDirectory = "jellyfin";
CacheDirectoryMode = "0700";
UMask = "0077";
ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
Restart = "on-failure";
# Security options:
NoNewPrivileges = true;
AmbientCapabilities = "";
CapabilityBoundingSet = "";
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
PrivateTmp = true;
# Disabled to allow Jellyfin to access hw accel devices endpoints
# PrivateDevices = true;
PrivateUsers = true;
# Disabled as it does not allow Jellyfin to interface with CUDA devices
# ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RemoveIPC = true;
RestrictNamespaces = true;
# AF_NETLINK needed because Jellyfin monitors the network connection
RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET" "AF_INET6" "AF_UNIX" ];
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
];
};
};
users.users = mkIf (cfg.user == "jellyfin") {
jellyfin = {
group = 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 lib.maintainers; [ minijackson ];
}

View file

@ -0,0 +1,117 @@
{ config, lib, pkgs, ... }:
with lib;
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 generators.mkValueStringDefault {} (head l)
else lib.concatMapStrings (s: "\n ${generators.mkValueStringDefault {} s}") l;
mkKeyValue = generators.mkKeyValueDefault {} ":";
};
in
{
##### interface
options = {
services.klipper = {
enable = mkEnableOption "Klipper, the 3D printer firmware";
package = mkOption {
type = types.package;
default = pkgs.klipper;
defaultText = literalExpression "pkgs.klipper";
description = "The Klipper package.";
};
inputTTY = mkOption {
type = types.path;
default = "/run/klipper/tty";
description = "Path of the virtual printer symlink to create.";
};
apiSocket = mkOption {
type = types.nullOr types.path;
default = "/run/klipper/api";
description = "Path of the API socket to create.";
};
octoprintIntegration = mkOption {
type = types.bool;
default = false;
description = "Allows Octoprint to control Klipper.";
};
user = mkOption {
type = types.nullOr 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 = mkOption {
type = types.nullOr 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 = mkOption {
type = format.type;
default = { };
description = ''
Configuration for Klipper. See the <link xlink:href="https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides">documentation</link>
for supported values.
'';
};
};
};
##### implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
message = "Option 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 klipper.group is not set when a user is specified.";
}
];
environment.etc."klipper.cfg".source = format.generate "klipper.cfg" cfg.settings;
services.klipper = mkIf cfg.octoprintIntegration {
user = config.services.octoprint.user;
group = config.services.octoprint.group;
};
systemd.services.klipper = let
klippyArgs = "--input-tty=${cfg.inputTTY}"
+ optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}";
in {
description = "Klipper 3D Printer Firmware";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/lib/klipper/klippy.py ${klippyArgs} /etc/klipper.cfg";
RuntimeDirectory = "klipper";
SupplementaryGroups = [ "dialout" ];
WorkingDirectory = "${cfg.package}/lib";
} // (if cfg.user != null then {
Group = cfg.group;
User = cfg.user;
} else {
DynamicUser = true;
User = "klipper";
});
};
};
}

View file

@ -0,0 +1,62 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.leaps;
stateDir = "/var/lib/leaps/";
in
{
options = {
services.leaps = {
enable = mkEnableOption "leaps";
port = mkOption {
type = types.port;
default = 8080;
description = "A port where leaps listens for incoming http requests";
};
address = mkOption {
default = "";
type = types.str;
example = "127.0.0.1";
description = "Hostname or IP-address to listen to. By default it will listen on all interfaces.";
};
path = mkOption {
default = "/";
type = types.path;
description = "Subdirectory used for reverse proxy setups";
};
};
};
config = 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,84 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.libreddit;
args = concatStringsSep " " ([
"--port ${toString cfg.port}"
"--address ${cfg.address}"
]);
in
{
options = {
services.libreddit = {
enable = mkEnableOption "Private front-end for Reddit";
address = mkOption {
default = "0.0.0.0";
example = "127.0.0.1";
type = types.str;
description = "The address to listen on";
};
port = mkOption {
default = 8080;
example = 8000;
type = types.port;
description = "The port to listen on";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the libreddit web interface";
};
};
};
config = mkIf cfg.enable {
systemd.services.libreddit = {
description = "Private front-end for Reddit";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${pkgs.libreddit}/bin/libreddit ${args}";
AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
Restart = "on-failure";
RestartSec = "2s";
# Hardening
CapabilityBoundingSet = if (cfg.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.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";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

View file

@ -0,0 +1,89 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.lidarr;
in
{
options = {
services.lidarr = {
enable = mkEnableOption "Lidarr";
dataDir = mkOption {
type = types.str;
default = "/var/lib/lidarr/.config/Lidarr";
description = "The directory where Lidarr stores its data files.";
};
package = mkOption {
type = types.package;
default = pkgs.lidarr;
defaultText = literalExpression "pkgs.lidarr";
description = "The Lidarr package to use";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for Lidarr
'';
};
user = mkOption {
type = types.str;
default = "lidarr";
description = ''
User account under which Lidarr runs.
'';
};
group = mkOption {
type = types.str;
default = "lidarr";
description = ''
Group under which Lidarr runs.
'';
};
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
];
systemd.services.lidarr = {
description = "Lidarr";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/Lidarr -nobrowser -data='${cfg.dataDir}'";
Restart = "on-failure";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 8686 ];
};
users.users = mkIf (cfg.user == "lidarr") {
lidarr = {
group = cfg.group;
home = "/var/lib/lidarr";
uid = config.ids.uids.lidarr;
};
};
users.groups = mkIf (cfg.group == "lidarr") {
lidarr = {
gid = config.ids.gids.lidarr;
};
};
};
}

View file

@ -0,0 +1,164 @@
{ config, pkgs, lib, ... }:
with 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 maintainers; [ cole-h grahamc ];
options = {
services.lifecycled = {
enable = mkEnableOption "lifecycled";
queueCleaner = {
enable = mkEnableOption "lifecycled-queue-cleaner";
frequency = mkOption {
type = 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 <literal>OnCalendar</literal> configuration. See
<citerefentry><refentrytitle>systemd.timer</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for more information.
'';
};
parallel = mkOption {
type = types.ints.unsigned;
default = 20;
description = ''
The number of parallel deletes to run.
'';
};
};
instanceId = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The instance ID to listen for events for.
'';
};
snsTopic = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The SNS topic that receives events.
'';
};
noSpot = mkOption {
type = types.bool;
default = false;
description = ''
Disable the spot termination listener.
'';
};
handler = mkOption {
type = types.path;
description = ''
The script to invoke to handle events.
'';
};
json = mkOption {
type = types.bool;
default = false;
description = ''
Enable JSON logging.
'';
};
cloudwatchGroup = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Write logs to a specific Cloudwatch Logs group.
'';
};
cloudwatchStream = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Write logs to a specific Cloudwatch Logs stream. Defaults to the instance ID.
'';
};
debug = mkOption {
type = types.bool;
default = false;
description = ''
Enable debugging information.
'';
};
# XXX: Can be removed if / when
# https://github.com/buildkite/lifecycled/pull/91 is merged.
awsRegion = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The region used for accessing AWS services.
'';
};
};
};
### Implementation ###
config = mkMerge [
(mkIf cfg.enable {
environment.etc."lifecycled".source = configFile;
systemd.packages = [ pkgs.lifecycled ];
systemd.services.lifecycled = {
wantedBy = [ "network-online.target" ];
restartTriggers = [ configFile ];
};
})
(mkIf cfg.queueCleaner.enable {
systemd.services.lifecycled-queue-cleaner = {
description = "Lifecycle Daemon Queue Cleaner";
environment = 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,30 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.logkeys;
in {
options.services.logkeys = {
enable = mkEnableOption "logkeys service";
device = mkOption {
description = "Use the given device as keyboard input event device instead of /dev/input/eventX default.";
default = null;
type = types.nullOr types.str;
example = "/dev/input/event15";
};
};
config = 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,69 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.mame;
mame = "mame${lib.optionalString pkgs.stdenv.is64bit "64"}";
in
{
options = {
services.mame = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to setup TUN/TAP Ethernet interface for MAME emulator.
'';
};
user = mkOption {
type = types.str;
description = ''
User from which you run MAME binary.
'';
};
hostAddr = mkOption {
type = 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 = mkOption {
type = 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</option>.
'';
example = "192.168.31.155";
};
};
};
config = 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 = with lib.maintainers; [ ];
}

View file

@ -0,0 +1,161 @@
{ config, options, pkgs, lib, ... }:
with lib;
let
dataDir = "/var/lib/matrix-appservice-discord";
registrationFile = "${dataDir}/discord-registration.yaml";
appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
cfg = config.services.matrix-appservice-discord;
opt = options.services.matrix-appservice-discord;
# TODO: switch to configGen.json once RFC42 is implemented
settingsFile = pkgs.writeText "matrix-appservice-discord-settings.json" (builtins.toJSON cfg.settings);
in {
options = {
services.matrix-appservice-discord = {
enable = mkEnableOption "a bridge between Matrix and Discord";
settings = mkOption rec {
# TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
type = types.attrs;
apply = recursiveUpdate default;
default = {
database = {
filename = "${dataDir}/discord.db";
};
# empty values necessary for registration file generation
# actual values defined in environmentFile
auth = {
clientID = "";
botToken = "";
};
};
example = literalExpression ''
{
bridge = {
domain = "public-domain.tld";
homeserverUrl = "http://public-domain.tld:8008";
};
}
'';
description = ''
<filename>config.yaml</filename> configuration as a Nix attribute set.
</para>
<para>
Configuration options should match those described in
<link xlink:href="https://github.com/Half-Shot/matrix-appservice-discord/blob/master/config/config.sample.yaml">
config.sample.yaml</link>.
</para>
<para>
<option>config.bridge.domain</option> and <option>config.bridge.homeserverUrl</option>
should be set to match the public host name of the Matrix homeserver for webhooks and avatars to work.
</para>
<para>
Secret tokens should be specified using <option>environmentFile</option>
instead of this world-readable attribute set.
'';
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
File containing environment variables to be passed to the matrix-appservice-discord service,
in which secret tokens can be specified securely by defining values for
<literal>APPSERVICE_DISCORD_AUTH_CLIENT_I_D</literal> and
<literal>APPSERVICE_DISCORD_AUTH_BOT_TOKEN</literal>.
'';
};
url = mkOption {
type = types.str;
default = "http://localhost:${toString cfg.port}";
defaultText = literalExpression ''"http://localhost:''${toString config.${opt.port}}"'';
description = ''
The URL where the application service is listening for HS requests.
'';
};
port = mkOption {
type = types.port;
default = 9005; # from https://github.com/Half-Shot/matrix-appservice-discord/blob/master/package.json#L11
description = ''
Port number on which the bridge should listen for internal communication with the Matrix homeserver.
'';
};
localpart = mkOption {
type = with types; nullOr str;
default = null;
description = ''
The user_id localpart to assign to the AS.
'';
};
serviceDependencies = mkOption {
type = with types; listOf str;
default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
defaultText = literalExpression ''
optional config.services.matrix-synapse.enable "matrix-synapse.service"
'';
description = ''
List of Systemd services to require and wait for when starting the application service,
such as the Matrix homeserver if it's running on the same host.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.matrix-appservice-discord = {
description = "A bridge between Matrix and Discord.";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
if [ ! -f '${registrationFile}' ]; then
${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
--generate-registration \
--url=${escapeShellArg cfg.url} \
${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \
--config='${settingsFile}' \
--file='${registrationFile}'
fi
'';
serviceConfig = {
Type = "simple";
Restart = "always";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
DynamicUser = true;
PrivateTmp = true;
WorkingDirectory = appDir;
StateDirectory = baseNameOf dataDir;
UMask = 0027;
EnvironmentFile = cfg.environmentFile;
ExecStart = ''
${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
--file='${registrationFile}' \
--config='${settingsFile}' \
--port='${toString cfg.port}'
'';
};
};
};
meta.maintainers = with maintainers; [ pacien ];
}

View file

@ -0,0 +1,232 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.matrix-appservice-irc;
pkg = pkgs.matrix-appservice-irc;
bin = "${pkg}/bin/matrix-appservice-irc";
jsonType = (pkgs.formats.json {}).type;
configFile = pkgs.runCommand "matrix-appservice-irc.yml" {
# Because this program will be run at build time, we need `nativeBuildInputs`
nativeBuildInputs = [ (pkgs.python3.withPackages (ps: [ ps.pyyaml ps.jsonschema ])) ];
preferLocalBuild = true;
config = builtins.toJSON cfg.settings;
passAsFile = [ "config" ];
} ''
# The schema is given as yaml, we need to convert it to json
python -c 'import json; import yaml; import sys; json.dump(yaml.safe_load(sys.stdin), sys.stdout)' \
< ${pkg}/lib/node_modules/matrix-appservice-irc/config.schema.yml \
> config.schema.json
python -m jsonschema config.schema.json -i $configPath
cp "$configPath" "$out"
'';
registrationFile = "/var/lib/matrix-appservice-irc/registration.yml";
in {
options.services.matrix-appservice-irc = with types; {
enable = mkEnableOption "the Matrix/IRC bridge";
port = mkOption {
type = port;
description = "The port to listen on";
default = 8009;
};
needBindingCap = mkOption {
type = bool;
description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)";
default = false;
};
passwordEncryptionKeyLength = mkOption {
type = ints.unsigned;
description = "Length of the key to encrypt IRC passwords with";
default = 4096;
example = 8192;
};
registrationUrl = mkOption {
type = str;
description = ''
The URL where the application service is listening for homeserver requests,
from the Matrix homeserver perspective.
'';
example = "http://localhost:8009";
};
localpart = mkOption {
type = str;
description = "The user_id localpart to assign to the appservice";
default = "appservice-irc";
};
settings = mkOption {
description = ''
Configuration for the appservice, see
<link xlink:href="https://github.com/matrix-org/matrix-appservice-irc/blob/${pkgs.matrix-appservice-irc.version}/config.sample.yaml"/>
for supported values
'';
default = {};
type = submodule {
freeformType = jsonType;
options = {
homeserver = mkOption {
description = "Homeserver configuration";
default = {};
type = submodule {
freeformType = jsonType;
options = {
url = mkOption {
type = str;
description = "The URL to the home server for client-server API calls";
};
domain = mkOption {
type = str;
description = ''
The 'domain' part for user IDs on this home server. Usually
(but not always) is the "domain name" part of the homeserver URL.
'';
};
};
};
};
database = mkOption {
default = {};
description = "Configuration for the database";
type = submodule {
freeformType = jsonType;
options = {
engine = mkOption {
type = str;
description = "Which database engine to use";
default = "nedb";
example = "postgres";
};
connectionString = mkOption {
type = str;
description = "The database connection string";
default = "nedb://var/lib/matrix-appservice-irc/data";
example = "postgres://username:password@host:port/databasename";
};
};
};
};
ircService = mkOption {
default = {};
description = "IRC bridge configuration";
type = submodule {
freeformType = jsonType;
options = {
passwordEncryptionKeyPath = mkOption {
type = str;
description = ''
Location of the key with which IRC passwords are encrypted
for storage. Will be generated on first run if not present.
'';
default = "/var/lib/matrix-appservice-irc/passkey.pem";
};
servers = mkOption {
type = submodule { freeformType = jsonType; };
description = "IRC servers to connect to";
};
};
};
};
};
};
};
};
config = mkIf cfg.enable {
systemd.services.matrix-appservice-irc = {
description = "Matrix-IRC bridge";
before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
wantedBy = [ "multi-user.target" ];
preStart = ''
umask 077
# Generate key for crypting passwords
if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then
${pkgs.openssl}/bin/openssl genpkey \
-out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \
-outform PEM \
-algorithm RSA \
-pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}"
fi
# Generate registration file
if ! [ -f "${registrationFile}" ]; then
# The easy case: the file has not been generated yet
${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
else
# The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to
# regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them.
# 1. Backup
id=$(grep "^id:.*$" ${registrationFile})
hs_token=$(grep "^hs_token:.*$" ${registrationFile})
as_token=$(grep "^as_token:.*$" ${registrationFile})
# 2. Regenerate
${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
# 3. Restore
sed -i "s/^id:.*$/$id/g" ${registrationFile}
sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile}
sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
fi
# Allow synapse access to the registration
if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
chgrp matrix-synapse ${registrationFile}
chmod g+r ${registrationFile}
fi
'';
serviceConfig = rec {
Type = "simple";
ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}";
ProtectHome = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
StateDirectory = "matrix-appservice-irc";
StateDirectoryMode = "755";
User = "matrix-appservice-irc";
Group = "matrix-appservice-irc";
CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE";
AmbientCapabilities = CapabilityBoundingSet;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
PrivateMounts = true;
SystemCallFilter = "~@aio @clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @setuid @swap";
SystemCallArchitectures = "native";
# AF_UNIX is required to connect to a postgres socket.
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
};
};
users.groups.matrix-appservice-irc = {};
users.users.matrix-appservice-irc = {
description = "Service user for the Matrix-IRC bridge";
group = "matrix-appservice-irc";
isSystemUser = true;
};
};
# uses attributes of the linked package
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,149 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.matrix-conduit;
format = pkgs.formats.toml {};
configFile = format.generate "conduit.toml" cfg.settings;
in
{
meta.maintainers = with maintainers; [ pstn piegames ];
options.services.matrix-conduit = {
enable = mkEnableOption "matrix-conduit";
extraEnvironment = mkOption {
type = types.attrsOf types.str;
description = "Extra Environment variables to pass to the conduit server.";
default = {};
example = { RUST_BACKTRACE="yes"; };
};
package = mkOption {
type = types.package;
default = pkgs.matrix-conduit;
defaultText = "pkgs.matrix-conduit";
example = "pkgs.matrix-conduit";
description = ''
Package of the conduit matrix server to use.
'';
};
settings = mkOption {
type = types.submodule {
freeformType = format.type;
options = {
global.server_name = mkOption {
type = types.str;
example = "example.com";
description = "The server_name is the name of this server. It is used as a suffix for user # and room ids.";
};
global.port = mkOption {
type = types.port;
default = 6167;
description = "The port Conduit will be running on. You need to set up a reverse proxy in your web server (e.g. apache or nginx), so all requests to /_matrix on port 443 and 8448 will be forwarded to the Conduit instance running on this port";
};
global.max_request_size = mkOption {
type = types.ints.positive;
default = 20000000;
description = "Max request size in bytes. Don't forget to also change it in the proxy.";
};
global.allow_registration = mkOption {
type = types.bool;
default = false;
description = "Whether new users can register on this server.";
};
global.allow_encryption = mkOption {
type = types.bool;
default = true;
description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
};
global.allow_federation = mkOption {
type = types.bool;
default = true;
description = ''
Whether this server federates with other servers.
'';
};
global.trusted_servers = mkOption {
type = types.listOf types.str;
default = [ "matrix.org" ];
description = "Servers trusted with signing server keys.";
};
global.address = mkOption {
type = types.str;
default = "::1";
description = "Address to listen on for connections by the reverse proxy/tls terminator.";
};
global.database_path = mkOption {
type = types.str;
default = "/var/lib/matrix-conduit/";
readOnly = true;
description = ''
Path to the conduit database, the directory where conduit will save its data.
Note that due to using the DynamicUser feature of systemd, this value should not be changed
and is set to be read only.
'';
};
global.database_backend = mkOption {
type = types.enum [ "sqlite" "rocksdb" ];
default = "sqlite";
example = "rocksdb";
description = ''
The database backend for the service. Switching it on an existing
instance will require manual migration of data.
'';
};
};
};
default = {};
description = ''
Generates the conduit.toml configuration file. Refer to
<link xlink:href="https://gitlab.com/famedly/conduit/-/blob/master/conduit-example.toml"/>
for details on supported values.
Note that database_path can not be edited because the service's reliance on systemd StateDir.
'';
};
};
config = mkIf cfg.enable {
systemd.services.conduit = {
description = "Conduit Matrix Server";
documentation = [ "https://gitlab.com/famedly/conduit/" ];
wantedBy = [ "multi-user.target" ];
environment = lib.mkMerge ([
{ CONDUIT_CONFIG = configFile; }
cfg.extraEnvironment
]);
serviceConfig = {
DynamicUser = true;
User = "conduit";
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateUsers = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
StateDirectory = "matrix-conduit";
ExecStart = "${cfg.package}/bin/conduit";
Restart = "on-failure";
RestartSec = 10;
StartLimitBurst = 5;
};
};
};
}

View file

@ -0,0 +1,195 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.mautrix-facebook;
settingsFormat = pkgs.formats.json {};
settingsFile = settingsFormat.generate "mautrix-facebook-config.json" cfg.settings;
puppetRegex = concatStringsSep
".*"
(map
escapeRegex
(splitString
"{userid}"
cfg.settings.bridge.username_template));
in {
options = {
services.mautrix-facebook = {
enable = mkEnableOption "Mautrix-Facebook, a Matrix-Facebook hybrid puppeting/relaybot bridge";
settings = mkOption rec {
apply = recursiveUpdate default;
type = settingsFormat.type;
default = {
homeserver = {
address = "http://localhost:8008";
};
appservice = rec {
address = "http://${hostname}:${toString port}";
hostname = "localhost";
port = 29319;
database = "postgresql://";
bot_username = "facebookbot";
};
metrics.enabled = false;
manhole.enabled = false;
bridge = {
encryption = {
allow = true;
default = true;
};
username_template = "facebook_{userid}";
};
logging = {
version = 1;
formatters.journal_fmt.format = "%(name)s: %(message)s";
handlers.journal = {
class = "systemd.journal.JournalHandler";
formatter = "journal_fmt";
SYSLOG_IDENTIFIER = "mautrix-facebook";
};
root = {
level = "INFO";
handlers = ["journal"];
};
};
};
example = literalExpression ''
{
homeserver = {
address = "http://localhost:8008";
domain = "mydomain.example";
};
bridge.permissions = {
"@admin:mydomain.example" = "admin";
"mydomain.example" = "user";
};
}
'';
description = ''
<filename>config.yaml</filename> configuration as a Nix attribute set.
Configuration options should match those described in
<link xlink:href="https://github.com/mautrix/facebook/blob/master/mautrix_facebook/example-config.yaml">
example-config.yaml</link>.
</para>
<para>
Secret tokens should be specified using <option>environmentFile</option>
instead of this world-readable attribute set.
'';
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
File containing environment variables to be passed to the mautrix-telegram service.
Any config variable can be overridden by setting <literal>MAUTRIX_FACEBOOK_SOME_KEY</literal> to override the <literal>some.key</literal> variable.
'';
};
configurePostgresql = mkOption {
type = types.bool;
default = true;
description = ''
Enable PostgreSQL and create a user and database for mautrix-facebook. The default <literal>settings</literal> reference this database, if you disable this option you must provide a database URL.
'';
};
registrationData = mkOption {
type = types.attrs;
default = {};
description = ''
Output data for appservice registration. Simply make any desired changes and serialize to JSON. Note that this data contains secrets so think twice before putting it into the nix store.
Currently <literal>as_token</literal> and <literal>hs_token</literal> need to be added as they are not known to this module.
'';
};
};
};
config = mkIf cfg.enable {
users.users.mautrix-facebook = {
group = "mautrix-facebook";
isSystemUser = true;
};
services.postgresql = mkIf cfg.configurePostgresql {
ensureDatabases = ["mautrix-facebook"];
ensureUsers = [{
name = "mautrix-facebook";
ensurePermissions = {
"DATABASE \"mautrix-facebook\"" = "ALL PRIVILEGES";
};
}];
};
systemd.services.mautrix-facebook = rec {
wantedBy = [ "multi-user.target" ];
wants = [
"network-online.target"
] ++ optional config.services.matrix-synapse.enable "matrix-synapse.service"
++ optional cfg.configurePostgresql "postgresql.service";
after = wants;
serviceConfig = {
Type = "simple";
Restart = "always";
User = "mautrix-facebook";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
PrivateTmp = true;
EnvironmentFile = cfg.environmentFile;
ExecStart = ''
${pkgs.mautrix-facebook}/bin/mautrix-facebook --config=${settingsFile}
'';
};
};
services.mautrix-facebook = {
registrationData = {
id = "mautrix-facebook";
namespaces = {
users = [
{
exclusive = true;
regex = escapeRegex "@${cfg.settings.appservice.bot_username}:${cfg.settings.homeserver.domain}";
}
{
exclusive = true;
regex = "@${puppetRegex}:${escapeRegex cfg.settings.homeserver.domain}";
}
];
aliases = [];
};
url = cfg.settings.appservice.address;
sender_localpart = "mautrix-facebook-sender";
rate_limited = false;
"de.sorunome.msc2409.push_ephemeral" = true;
push_ephemeral = true;
};
};
};
meta.maintainers = with maintainers; [ kevincox ];
}

View file

@ -0,0 +1,181 @@
{ config, pkgs, lib, ... }:
with lib;
let
dataDir = "/var/lib/mautrix-telegram";
registrationFile = "${dataDir}/telegram-registration.yaml";
cfg = config.services.mautrix-telegram;
settingsFormat = pkgs.formats.json {};
settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config-unsubstituted.json" cfg.settings;
settingsFile = "${dataDir}/config.json";
in {
options = {
services.mautrix-telegram = {
enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
settings = mkOption rec {
apply = recursiveUpdate default;
inherit (settingsFormat) type;
default = {
appservice = rec {
database = "sqlite:///${dataDir}/mautrix-telegram.db";
database_opts = {};
hostname = "0.0.0.0";
port = 8080;
address = "http://localhost:${toString port}";
};
bridge = {
permissions."*" = "relaybot";
relaybot.whitelist = [ ];
double_puppet_server_map = {};
login_shared_secret_map = {};
};
logging = {
version = 1;
formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
handlers.console = {
class = "logging.StreamHandler";
formatter = "precise";
};
loggers = {
mau.level = "INFO";
telethon.level = "INFO";
# prevent tokens from leaking in the logs:
# https://github.com/tulir/mautrix-telegram/issues/351
aiohttp.level = "WARNING";
};
# log to console/systemd instead of file
root = {
level = "INFO";
handlers = [ "console" ];
};
};
};
example = literalExpression ''
{
homeserver = {
address = "http://localhost:8008";
domain = "public-domain.tld";
};
appservice.public = {
prefix = "/public";
external = "https://public-appservice-address/public";
};
bridge.permissions = {
"example.com" = "full";
"@admin:example.com" = "admin";
};
}
'';
description = ''
<filename>config.yaml</filename> configuration as a Nix attribute set.
Configuration options should match those described in
<link xlink:href="https://github.com/tulir/mautrix-telegram/blob/master/example-config.yaml">
example-config.yaml</link>.
</para>
<para>
Secret tokens should be specified using <option>environmentFile</option>
instead of this world-readable attribute set.
'';
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
File containing environment variables to be passed to the mautrix-telegram service,
in which secret tokens can be specified securely by defining values for
<literal>MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN</literal>,
<literal>MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN</literal>,
<literal>MAUTRIX_TELEGRAM_TELEGRAM_API_ID</literal>,
<literal>MAUTRIX_TELEGRAM_TELEGRAM_API_HASH</literal> and optionally
<literal>MAUTRIX_TELEGRAM_TELEGRAM_BOT_TOKEN</literal>.
'';
};
serviceDependencies = mkOption {
type = with types; listOf str;
default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
defaultText = literalExpression ''
optional config.services.matrix-synapse.enable "matrix-synapse.service"
'';
description = ''
List of Systemd services to require and wait for when starting the application service.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.mautrix-telegram = {
description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
# Not all secrets can be passed as environment variable (yet)
# https://github.com/tulir/mautrix-telegram/issues/584
[ -f ${settingsFile} ] && rm -f ${settingsFile}
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o ${settingsFile} \
-i ${settingsFileUnsubstituted}
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${pkgs.mautrix-telegram}/bin/mautrix-telegram \
--generate-registration \
--base-config='${pkgs.mautrix-telegram}/${pkgs.mautrix-telegram.pythonModule.sitePackages}/mautrix_telegram/example-config.yaml' \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
'' + lib.optionalString (pkgs.mautrix-telegram ? alembic) ''
# run automatic database init and migration scripts
${pkgs.mautrix-telegram.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
'';
serviceConfig = {
Type = "simple";
Restart = "always";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
DynamicUser = true;
PrivateTmp = true;
WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
StateDirectory = baseNameOf dataDir;
UMask = 0027;
EnvironmentFile = cfg.environmentFile;
ExecStart = ''
${pkgs.mautrix-telegram}/bin/mautrix-telegram \
--config='${settingsFile}'
'';
};
restartTriggers = [ settingsFileUnsubstituted ];
};
};
meta.maintainers = with maintainers; [ pacien vskilet ];
}

View file

@ -0,0 +1,101 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.mbpfan;
verbose = if cfg.verbose then "v" else "";
settingsFormat = pkgs.formats.ini {};
settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
in {
options.services.mbpfan = {
enable = mkEnableOption "mbpfan, fan controller daemon for Apple Macs and MacBooks";
package = mkOption {
type = types.package;
default = pkgs.mbpfan;
defaultText = literalExpression "pkgs.mbpfan";
description = ''
The package used for the mbpfan daemon.
'';
};
verbose = mkOption {
type = types.bool;
default = false;
description = ''
If true, sets the log level to verbose.
'';
};
settings = mkOption {
default = {};
description = "INI configuration for Mbpfan.";
type = types.submodule {
freeformType = settingsFormat.type;
options.general.min_fan1_speed = mkOption {
type = types.nullOr types.int;
default = 2000;
description = ''
You can check minimum and maximum fan limits with
"cat /sys/devices/platform/applesmc.768/fan*_min" and
"cat /sys/devices/platform/applesmc.768/fan*_max" respectively.
Setting to null implies using default value from applesmc.
'';
};
options.general.low_temp = mkOption {
type = types.int;
default = 55;
description = "If temperature is below this, fans will run at minimum speed.";
};
options.general.high_temp = mkOption {
type = types.int;
default = 58;
description = "If temperature is above this, fan speed will gradually increase.";
};
options.general.max_temp = mkOption {
type = types.int;
default = 86;
description = "If temperature is above this, fans will run at maximum speed.";
};
options.general.polling_interval = mkOption {
type = types.int;
default = 1;
description = "The polling interval.";
};
};
};
};
imports = [
(mkRenamedOptionModule [ "services" "mbpfan" "pollingInterval" ] [ "services" "mbpfan" "settings" "general" "polling_interval" ])
(mkRenamedOptionModule [ "services" "mbpfan" "maxTemp" ] [ "services" "mbpfan" "settings" "general" "max_temp" ])
(mkRenamedOptionModule [ "services" "mbpfan" "lowTemp" ] [ "services" "mbpfan" "settings" "general" "low_temp" ])
(mkRenamedOptionModule [ "services" "mbpfan" "highTemp" ] [ "services" "mbpfan" "settings" "general" "high_temp" ])
(mkRenamedOptionModule [ "services" "mbpfan" "minFanSpeed" ] [ "services" "mbpfan" "settings" "general" "min_fan1_speed" ])
(mkRenamedOptionModule [ "services" "mbpfan" "maxFanSpeed" ] [ "services" "mbpfan" "settings" "general" "max_fan1_speed" ])
];
config = mkIf cfg.enable {
boot.kernelModules = [ "coretemp" "applesmc" ];
environment.etc."mbpfan.conf".source = settingsFile;
environment.systemPackages = [ cfg.package ];
systemd.services.mbpfan = {
description = "A fan manager daemon for MacBook Pro";
wantedBy = [ "sysinit.target" ];
after = [ "syslog.target" "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,394 @@
{ config, lib, options, pkgs, ... }:
with lib;
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 = mkOption {
type = types.str;
description = ''
Absolute directory path to the media directory to index.
'';
};
recursive = mkOption {
type = types.bool;
default = false;
description = "Whether the indexation must take place recursively or not.";
};
hidden-files = mkOption {
type = 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" />
<transcode mimetype="audio/x-flac" using="oggflac2raw"/>
</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="${libsForQt5.vlc}/bin/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 = 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}"/>
${optionalString cfg.dsmSupport ''
<custom-http-headers>
<add header="X-User-Agent: redsonic"/>
</custom-http-headers>
<manufacturerURL>redsonic.com</manufacturerURL>
<modelNumber>105</modelNumber>
''}
${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">
${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"/>
${optionalString cfg.ps3Support ''
<map from="avi" to="video/divx"/>
''}
${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.
# http://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 = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the Gerbera/Mediatomb DLNA server.
'';
};
serverName = mkOption {
type = types.str;
default = "Gerbera (Mediatomb)";
description = ''
How to identify the server on the network.
'';
};
package = mkOption {
type = types.package;
default = pkgs.gerbera;
defaultText = literalExpression "pkgs.gerbera";
description = ''
Underlying package to be used with the module.
'';
};
ps3Support = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable ps3 specific tweaks.
WARNING: incompatible with DSM 320 support.
'';
};
dsmSupport = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable D-Link DSM 320 specific tweaks.
WARNING: incompatible with ps3 support.
'';
};
tg100Support = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Telegent TG100 specific tweaks.
'';
};
transcoding = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable transcoding.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/${name}";
defaultText = literalExpression ''"/var/lib/''${config.${opt.package}.pname}"'';
description = ''
The directory where Gerbera/Mediatomb stores its state, data, etc.
'';
};
pcDirectoryHide = mkOption {
type = types.bool;
default = true;
description = ''
Whether to list the top-level directory or not (from upnp client standpoint).
'';
};
user = mkOption {
type = types.str;
default = "mediatomb";
description = "User account under which the service runs.";
};
group = mkOption {
type = types.str;
default = "mediatomb";
description = "Group account under which the service runs.";
};
port = mkOption {
type = types.int;
default = 49152;
description = ''
The network port to listen on.
'';
};
interface = mkOption {
type = types.str;
default = "";
description = ''
A specific interface to bind to.
'';
};
openFirewall = mkOption {
type = 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</option>.
If the option <option>services.mediatomb.interface</option> is set,
the firewall rules opened are dedicated to that interface. Otherwise,
those rules are opened globally.
'';
};
uuid = mkOption {
type = types.str;
default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
description = ''
A unique (on your network) to identify the server by.
'';
};
mediaDirectories = mkOption {
type = with 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 = mkOption {
type = types.bool;
default = false;
description = ''
Allow the service to create and use its own config file inside the <literal>dataDir</literal> as
configured by <option>services.mediatomb.dataDir</option>.
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 <literal>dataDir</literal>. It's up to the user to make a correct
configuration file.
'';
};
};
};
###### implementation
config = let binaryCommand = "${pkg}/bin/${name}";
interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
in mkIf cfg.enable {
systemd.services.mediatomb = {
description = "${cfg.serverName} media Server";
after = [ "network.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 = optionalAttrs (cfg.group == "mediatomb") {
mediatomb.gid = gid;
};
users.users = 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 = mkMerge [
(mkIf (cfg.openFirewall && cfg.interface != "") {
interfaces."${cfg.interface}" = defaultFirewallRules;
})
(mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
];
};
}

View file

@ -0,0 +1,103 @@
{ 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";
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 = ''
<link xlink:href="https://www.digitalocean.com/community/tutorials/java-keytool-essentials-working-with-java-keystores">Java KeyStore</link> 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" ];
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 = "${pkgs.metabase}/bin/metabase";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.listen.port ] ++ optional cfg.ssl.enable cfg.ssl.port;
};
};
}

View file

@ -0,0 +1,178 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
pkg = pkgs.moonraker;
cfg = config.services.moonraker;
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 generators.mkValueStringDefault {} (head l)
else lib.concatMapStrings (s: "\n ${generators.mkValueStringDefault {} s}") l;
mkKeyValue = generators.mkKeyValueDefault {} ":";
};
in {
options = {
services.moonraker = {
enable = mkEnableOption "Moonraker, an API web server for Klipper";
klipperSocket = mkOption {
type = types.path;
default = config.services.klipper.apiSocket;
defaultText = literalExpression "config.services.klipper.apiSocket";
description = "Path to Klipper's API socket.";
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/moonraker";
description = "The directory containing the Moonraker databases.";
};
configDir = mkOption {
type = types.path;
default = cfg.stateDir + "/config";
defaultText = literalExpression ''config.${opt.stateDir} + "/config"'';
description = ''
The 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 = mkOption {
type = types.str;
default = "moonraker";
description = "User account under which Moonraker runs.";
};
group = mkOption {
type = types.str;
default = "moonraker";
description = "Group account under which Moonraker runs.";
};
address = mkOption {
type = types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "The IP or host to listen on.";
};
port = mkOption {
type = types.ints.unsigned;
default = 7125;
description = "The port to listen on.";
};
settings = mkOption {
type = format.type;
default = { };
example = {
authorization = {
trusted_clients = [ "10.0.0.0/24" ];
cors_domains = [ "https://app.fluidd.xyz" ];
};
};
description = ''
Configuration for Moonraker. See the <link xlink:href="https://moonraker.readthedocs.io/en/latest/configuration/">documentation</link>
for supported values.
'';
};
allowSystemControl = mkOption {
type = 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
<link xlink:href="https://moonraker.readthedocs.io/en/latest/web_api/#machine-commands">documentation</link>
for details on what clients are able to do.
'';
};
};
};
config = mkIf cfg.enable {
warnings = optional (cfg.settings ? update_manager)
''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.'';
assertions = [
{
assertion = cfg.allowSystemControl -> config.security.polkit.enable;
message = "services.moonraker.allowSystemControl requires polkit to be enabled (security.polkit.enable).";
}
];
users.users = optionalAttrs (cfg.user == "moonraker") {
moonraker = {
group = cfg.group;
uid = config.ids.uids.moonraker;
};
};
users.groups = 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;
config_path = cfg.configDir;
database_path = "${cfg.stateDir}/database";
};
};
fullConfig = recursiveUpdate cfg.settings forcedConfig;
in format.generate "moonraker.cfg" fullConfig;
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
"d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -"
];
systemd.services.moonraker = {
description = "Moonraker, an API web server for Klipper";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]
++ optional config.services.klipper.enable "klipper.service";
# Moonraker really wants its own config to be writable...
script = ''
cp /etc/moonraker.cfg ${cfg.configDir}/moonraker-temp.cfg
chmod u+w ${cfg.configDir}/moonraker-temp.cfg
exec ${pkg}/bin/moonraker -c ${cfg.configDir}/moonraker-temp.cfg
'';
# Needs `ip` command
path = [ pkgs.iproute2 ];
serviceConfig = {
WorkingDirectory = cfg.stateDir;
Group = cfg.group;
User = cfg.user;
};
};
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;
}
});
'';
};
}

View file

@ -0,0 +1,122 @@
{ config, pkgs, lib, ... }:
with lib;
let
dataDir = "/var/lib/mx-puppet-discord";
registrationFile = "${dataDir}/discord-registration.yaml";
cfg = config.services.mx-puppet-discord;
settingsFormat = pkgs.formats.json {};
settingsFile = settingsFormat.generate "mx-puppet-discord-config.json" cfg.settings;
in {
options = {
services.mx-puppet-discord = {
enable = mkEnableOption ''
mx-puppet-discord is a discord puppeting bridge for matrix.
It handles bridging private and group DMs, as well as Guilds (servers)
'';
settings = mkOption rec {
apply = recursiveUpdate default;
inherit (settingsFormat) type;
default = {
bridge.port = 8434;
presence = {
enabled = true;
interval = 500;
};
provisioning.whitelist = [ ];
relay.whitelist = [ ];
# variables are preceded by a colon.
namePatterns = {
user = ":name";
userOverride = ":displayname";
room = ":name";
group = ":name";
};
#defaults to sqlite but can be configured to use postgresql with
#connstring
database.filename = "${dataDir}/database.db";
logging = {
console = "info";
lineDateFormat = "MMM-D HH:mm:ss.SSS";
};
};
example = literalExpression ''
{
bridge = {
bindAddress = "localhost";
domain = "example.com";
homeserverUrl = "https://example.com";
};
provisioning.whitelist = [ "@admin:example.com" ];
relay.whitelist = [ "@.*:example.com" ];
}
'';
description = ''
<filename>config.yaml</filename> configuration as a Nix attribute set.
Configuration options should match those described in
<link xlink:href="https://github.com/matrix-discord/mx-puppet-discord/blob/master/sample.config.yaml">
sample.config.yaml</link>.
'';
};
serviceDependencies = mkOption {
type = with types; listOf str;
default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
defaultText = literalExpression ''
optional config.services.matrix-synapse.enable "matrix-synapse.service"
'';
description = ''
List of Systemd services to require and wait for when starting the application service.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.mx-puppet-discord = {
description = "Matrix to Discord puppeting bridge";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${pkgs.mx-puppet-discord}/bin/mx-puppet-discord -r -c ${settingsFile} \
-f ${registrationFile}
fi
'';
serviceConfig = {
Type = "simple";
Restart = "always";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
DynamicUser = true;
PrivateTmp = true;
WorkingDirectory = pkgs.mx-puppet-discord;
StateDirectory = baseNameOf dataDir;
UMask = 0027;
ExecStart = ''
${pkgs.mx-puppet-discord}/bin/mx-puppet-discord \
-c ${settingsFile} \
-f ${registrationFile}
'';
};
};
};
meta.maintainers = with maintainers; [ govanify ];
}

View file

@ -0,0 +1,79 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.n8n;
format = pkgs.formats.json {};
configFile = format.generate "n8n.json" cfg.settings;
in
{
options.services.n8n = {
enable = mkEnableOption "n8n server";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the n8n web interface.";
};
settings = mkOption {
type = format.type;
default = {};
description = ''
Configuration for n8n, see <link xlink:href="https://docs.n8n.io/reference/configuration.html"/>
for supported values.
'';
};
};
config = 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}";
};
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 = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.settings.port ];
};
};
}

View file

@ -0,0 +1,358 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.nitter;
configFile = pkgs.writeText "nitter.conf" ''
${generators.toINI {
# String values need to be quoted
mkKeyValue = generators.mkKeyValueDefault {
mkValueString = v:
if isString v then "\"" + (strings.escape ["\""] (toString v)) + "\""
else 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
{
options = {
services.nitter = {
enable = mkEnableOption "If enabled, start Nitter.";
package = mkOption {
default = pkgs.nitter;
type = types.package;
defaultText = literalExpression "pkgs.nitter";
description = "The nitter derivation to use.";
};
server = {
address = mkOption {
type = types.str;
default = "0.0.0.0";
example = "127.0.0.1";
description = "The address to listen on.";
};
port = mkOption {
type = types.port;
default = 8080;
example = 8000;
description = "The port to listen on.";
};
https = mkOption {
type = types.bool;
default = false;
description = "Set secure attribute on cookies. Keep it disabled to enable cookies when not using HTTPS.";
};
httpMaxConnections = mkOption {
type = types.int;
default = 100;
description = "Maximum number of HTTP connections.";
};
staticDir = mkOption {
type = types.path;
default = "${cfg.package}/share/nitter/public";
defaultText = literalExpression ''"''${config.services.nitter.package}/share/nitter/public"'';
description = "Path to the static files directory.";
};
title = mkOption {
type = types.str;
default = "nitter";
description = "Title of the instance.";
};
hostname = mkOption {
type = types.str;
default = "localhost";
example = "nitter.net";
description = "Hostname of the instance.";
};
};
cache = {
listMinutes = mkOption {
type = types.int;
default = 240;
description = "How long to cache list info (not the tweets, so keep it high).";
};
rssMinutes = mkOption {
type = types.int;
default = 10;
description = "How long to cache RSS queries.";
};
redisHost = mkOption {
type = types.str;
default = "localhost";
description = "Redis host.";
};
redisPort = mkOption {
type = types.port;
default = 6379;
description = "Redis port.";
};
redisConnections = mkOption {
type = types.int;
default = 20;
description = "Redis connection pool size.";
};
redisMaxConnections = mkOption {
type = 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 = mkOption {
type = types.bool;
default = false;
description = "Use base64 encoding for proxied media URLs.";
};
tokenCount = mkOption {
type = 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 = mkOption {
type = types.str;
default = "";
example = "nitter.net";
description = "Replace Twitter links with links to this instance (blank to disable).";
};
replaceYouTube = mkOption {
type = types.str;
default = "";
example = "piped.kavin.rocks";
description = "Replace YouTube links with links to this instance (blank to disable).";
};
replaceInstagram = mkOption {
type = types.str;
default = "";
description = "Replace Instagram links with links to this instance (blank to disable).";
};
mp4Playback = mkOption {
type = types.bool;
default = true;
description = "Enable MP4 video playback.";
};
hlsPlayback = mkOption {
type = types.bool;
default = false;
description = "Enable HLS video streaming (requires JavaScript).";
};
proxyVideos = mkOption {
type = types.bool;
default = true;
description = "Proxy video streaming through the server (might be slow).";
};
muteVideos = mkOption {
type = types.bool;
default = false;
description = "Mute videos by default.";
};
autoplayGifs = mkOption {
type = types.bool;
default = true;
description = "Autoplay GIFs.";
};
theme = mkOption {
type = types.str;
default = "Nitter";
description = "Instance theme.";
};
infiniteScroll = mkOption {
type = types.bool;
default = false;
description = "Infinite scrolling (requires JavaScript, experimental!).";
};
stickyProfile = mkOption {
type = types.bool;
default = true;
description = "Make profile sidebar stick to top.";
};
bidiSupport = mkOption {
type = types.bool;
default = false;
description = "Support bidirectional text (makes clicking on tweets harder).";
};
hideTweetStats = mkOption {
type = types.bool;
default = false;
description = "Hide tweet stats (replies, retweets, likes).";
};
hideBanner = mkOption {
type = types.bool;
default = false;
description = "Hide profile banner.";
};
hidePins = mkOption {
type = types.bool;
default = false;
description = "Hide pinned tweets.";
};
hideReplies = mkOption {
type = types.bool;
default = false;
description = "Hide tweet replies.";
};
};
settings = mkOption {
type = 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
'';
};
redisCreateLocally = mkOption {
type = types.bool;
default = true;
description = "Configure local Redis server for Nitter.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for Nitter web interface.";
};
};
};
config = 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" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "nitter";
Environment = [ "NITTER_CONF_FILE=/var/lib/nitter/nitter.conf" ];
# 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 = lib.mkIf (cfg.redisCreateLocally) {
enable = true;
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.server.port ];
};
};
}

View file

@ -0,0 +1,826 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.nix;
nixPackage = cfg.package.out;
isNixAtLeast = versionAtLeast (getVersion nixPackage);
makeNixBuildUser = nr: {
name = "nixbld${toString nr}";
value = {
description = "Nix build user ${toString nr}";
/*
For consistency with the setgid(2), setuid(2), and setgroups(2)
calls in `libstore/build.cc', don't add any supplementary group
here except "nixbld".
*/
uid = builtins.add config.ids.uids.nixbld nr;
isSystemUser = true;
group = "nixbld";
extraGroups = [ "nixbld" ];
};
};
nixbldUsers = listToAttrs (map makeNixBuildUser (range 1 cfg.nrBuildUsers));
nixConf =
assert isNixAtLeast "2.2";
let
mkValueString = v:
if v == null then ""
else if isInt v then toString v
else if isBool v then boolToString v
else if isFloat v then floatToString v
else if isList v then toString v
else if isDerivation v then toString v
else if builtins.isPath v then toString v
else if isString v then v
else if isCoercibleToString v then toString v
else abort "The nix conf value: ${toPretty {} v} can not be encoded";
mkKeyValue = k: v: "${escape [ "=" ] k} = ${mkValueString v}";
mkKeyValuePairs = attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValue attrs);
in
pkgs.writeTextFile {
name = "nix.conf";
text = ''
# WARNING: this file is generated from the nix.* options in
# your NixOS configuration, typically
# /etc/nixos/configuration.nix. Do not edit it!
${mkKeyValuePairs cfg.settings}
${cfg.extraOptions}
'';
checkPhase =
if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then ''
echo "Ignoring validation for cross-compilation"
''
else ''
echo "Validating generated nix.conf"
ln -s $out ./nix.conf
set -e
set +o pipefail
NIX_CONF_DIR=$PWD \
${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \
${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \
|& sed -e 's/^warning:/error:/' \
| (! grep '${if cfg.checkConfig then "^error:" else "^error: unknown setting"}')
set -o pipefail
'';
};
legacyConfMappings = {
useSandbox = "sandbox";
buildCores = "cores";
maxJobs = "max-jobs";
sandboxPaths = "extra-sandbox-paths";
binaryCaches = "substituters";
trustedBinaryCaches = "trusted-substituters";
binaryCachePublicKeys = "trusted-public-keys";
autoOptimiseStore = "auto-optimise-store";
requireSignedBinaryCaches = "require-sigs";
trustedUsers = "trusted-users";
allowedUsers = "allowed-users";
systemFeatures = "system-features";
};
semanticConfType = with types;
let
confAtom = nullOr
(oneOf [
bool
int
float
str
path
package
]) // {
description = "Nix config atom (null, bool, int, float, str, path or package)";
};
in
attrsOf (either confAtom (listOf confAtom));
in
{
imports = [
(mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "useChroot" ]; to = [ "nix" "useSandbox" ]; })
(mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "chrootDirs" ]; to = [ "nix" "sandboxPaths" ]; })
(mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" "daemonIONiceLevel" ]; to = [ "nix" "daemonIOSchedPriority" ]; })
(mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.")
] ++ mapAttrsToList (oldConf: newConf: mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" oldConf ]; to = [ "nix" "settings" newConf ]; }) legacyConfMappings;
###### interface
options = {
nix = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable Nix.
Disabling Nix makes the system hard to modify and the Nix programs and configuration will not be made available by NixOS itself.
'';
};
package = mkOption {
type = types.package;
default = pkgs.nix;
defaultText = literalExpression "pkgs.nix";
description = ''
This option specifies the Nix package instance to use throughout the system.
'';
};
distributedBuilds = mkOption {
type = types.bool;
default = false;
description = ''
Whether to distribute builds to the machines listed in
<option>nix.buildMachines</option>.
'';
};
daemonCPUSchedPolicy = mkOption {
type = types.enum [ "other" "batch" "idle" ];
default = "other";
example = "batch";
description = ''
Nix daemon process CPU scheduling policy. This policy propagates to
build processes. <literal>other</literal> is the default scheduling
policy for regular tasks. The <literal>batch</literal> policy is
similar to <literal>other</literal>, but optimised for
non-interactive tasks. <literal>idle</literal> is for extremely
low-priority tasks that should only be run when no other task
requires CPU time.
Please note that while using the <literal>idle</literal> policy may
greatly improve responsiveness of a system performing expensive
builds, it may also slow down and potentially starve crucial
configuration updates during load.
<literal>idle</literal> may therefore be a sensible policy for
systems that experience only intermittent phases of high CPU load,
such as desktop or portable computers used interactively. Other
systems should use the <literal>other</literal> or
<literal>batch</literal> policy instead.
For more fine-grained resource control, please refer to
<citerefentry><refentrytitle>systemd.resource-control
</refentrytitle><manvolnum>5</manvolnum></citerefentry> and adjust
<option>systemd.services.nix-daemon</option> directly.
'';
};
daemonIOSchedClass = mkOption {
type = types.enum [ "best-effort" "idle" ];
default = "best-effort";
example = "idle";
description = ''
Nix daemon process I/O scheduling class. This class propagates to
build processes. <literal>best-effort</literal> is the default
class for regular tasks. The <literal>idle</literal> class is for
extremely low-priority tasks that should only perform I/O when no
other task does.
Please note that while using the <literal>idle</literal> scheduling
class can improve responsiveness of a system performing expensive
builds, it might also slow down or starve crucial configuration
updates during load.
<literal>idle</literal> may therefore be a sensible class for
systems that experience only intermittent phases of high I/O load,
such as desktop or portable computers used interactively. Other
systems should use the <literal>best-effort</literal> class.
'';
};
daemonIOSchedPriority = mkOption {
type = types.int;
default = 0;
example = 1;
description = ''
Nix daemon process I/O scheduling priority. This priority propagates
to build processes. The supported priorities depend on the
scheduling policy: With idle, priorities are not used in scheduling
decisions. best-effort supports values in the range 0 (high) to 7
(low).
'';
};
buildMachines = mkOption {
type = types.listOf (types.submodule {
options = {
hostName = mkOption {
type = types.str;
example = "nixbuilder.example.org";
description = ''
The hostname of the build machine.
'';
};
system = mkOption {
type = types.nullOr types.str;
default = null;
example = "x86_64-linux";
description = ''
The system type the build machine can execute derivations on.
Either this attribute or <varname>systems</varname> must be
present, where <varname>system</varname> takes precedence if
both are set.
'';
};
systems = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "x86_64-linux" "aarch64-linux" ];
description = ''
The system types the build machine can execute derivations on.
Either this attribute or <varname>system</varname> must be
present, where <varname>system</varname> takes precedence if
both are set.
'';
};
sshUser = mkOption {
type = types.nullOr types.str;
default = null;
example = "builder";
description = ''
The username to log in as on the remote host. This user must be
able to log in and run nix commands non-interactively. It must
also be privileged to build derivations, so must be included in
<option>nix.settings.trusted-users</option>.
'';
};
sshKey = mkOption {
type = types.nullOr types.str;
default = null;
example = "/root/.ssh/id_buildhost_builduser";
description = ''
The path to the SSH private key with which to authenticate on
the build machine. The private key must not have a passphrase.
If null, the building user (root on NixOS machines) must have an
appropriate ssh configuration to log in non-interactively.
Note that for security reasons, this path must point to a file
in the local filesystem, *not* to the nix store.
'';
};
maxJobs = mkOption {
type = types.int;
default = 1;
description = ''
The number of concurrent jobs the build machine supports. The
build machine will enforce its own limits, but this allows hydra
to schedule better since there is no work-stealing between build
machines.
'';
};
speedFactor = mkOption {
type = types.int;
default = 1;
description = ''
The relative speed of this builder. This is an arbitrary integer
that indicates the speed of this builder, relative to other
builders. Higher is faster.
'';
};
mandatoryFeatures = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "big-parallel" ];
description = ''
A list of features mandatory for this builder. The builder will
be ignored for derivations that don't require all features in
this list. All mandatory features are automatically included in
<varname>supportedFeatures</varname>.
'';
};
supportedFeatures = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "kvm" "big-parallel" ];
description = ''
A list of features supported by this builder. The builder will
be ignored for derivations that require features not in this
list.
'';
};
publicHostKey = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The (base64-encoded) public host key of this builder. The field
is calculated via <command>base64 -w0 /etc/ssh/ssh_host_type_key.pub</command>.
If null, SSH will use its regular known-hosts file when connecting.
'';
};
};
});
default = [ ];
description = ''
This option lists the machines to be used if distributed builds are
enabled (see <option>nix.distributedBuilds</option>).
Nix will perform derivations on those machines via SSH by copying the
inputs to the Nix store on the remote machine, starting the build,
then copying the output back to the local Nix store.
'';
};
# Environment variables for running Nix.
envVars = mkOption {
type = types.attrs;
internal = true;
default = { };
description = "Environment variables used by Nix.";
};
nrBuildUsers = mkOption {
type = types.int;
description = ''
Number of <literal>nixbld</literal> user accounts created to
perform secure concurrent builds. If you receive an error
message saying that all build users are currently in use,
you should increase this value.
'';
};
readOnlyStore = mkOption {
type = types.bool;
default = true;
description = ''
If set, NixOS will enforce the immutability of the Nix store
by making <filename>/nix/store</filename> a read-only bind
mount. Nix will automatically make the store writable when
needed.
'';
};
nixPath = mkOption {
type = types.listOf types.str;
default = [
"nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos"
"nixos-config=/etc/nixos/configuration.nix"
"/nix/var/nix/profiles/per-user/root/channels"
];
description = ''
The default Nix expression search path, used by the Nix
evaluator to look up paths enclosed in angle brackets
(e.g. <literal>&lt;nixpkgs&gt;</literal>).
'';
};
checkConfig = mkOption {
type = types.bool;
default = true;
description = ''
If enabled (the default), checks for data type mismatches and that Nix
can parse the generated nix.conf.
'';
};
registry = mkOption {
type = types.attrsOf (types.submodule (
let
referenceAttrs = with types; attrsOf (oneOf [
str
int
bool
package
]);
in
{ config, name, ... }:
{
options = {
from = mkOption {
type = referenceAttrs;
example = { type = "indirect"; id = "nixpkgs"; };
description = "The flake reference to be rewritten.";
};
to = mkOption {
type = referenceAttrs;
example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
description = "The flake reference <option>from</option> is rewritten to.";
};
flake = mkOption {
type = types.nullOr types.attrs;
default = null;
example = literalExpression "nixpkgs";
description = ''
The flake input <option>from</option> is rewritten to.
'';
};
exact = mkOption {
type = types.bool;
default = true;
description = ''
Whether the <option>from</option> reference needs to match exactly. If set,
a <option>from</option> reference like <literal>nixpkgs</literal> does not
match with a reference like <literal>nixpkgs/nixos-20.03</literal>.
'';
};
};
config = {
from = mkDefault { type = "indirect"; id = name; };
to = mkIf (config.flake != null) (mkDefault
{
type = "path";
path = config.flake.outPath;
} // filterAttrs
(n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
config.flake);
};
}
));
default = { };
description = ''
A system-wide flake registry.
'';
};
extraOptions = mkOption {
type = types.lines;
default = "";
example = ''
keep-outputs = true
keep-derivations = true
'';
description = "Additional text appended to <filename>nix.conf</filename>.";
};
settings = mkOption {
type = types.submodule {
freeformType = semanticConfType;
options = {
max-jobs = mkOption {
type = types.either types.int (types.enum [ "auto" ]);
default = "auto";
example = 64;
description = ''
This option defines the maximum number of jobs that Nix will try to
build in parallel. The default is auto, which means it will use all
available logical cores. It is recommend to set it to the total
number of logical cores in your system (e.g., 16 for two CPUs with 4
cores each and hyper-threading).
'';
};
auto-optimise-store = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
If set to true, Nix automatically detects files in the store that have
identical contents, and replaces them with hard links to a single copy.
This saves disk space. If set to false (the default), you can still run
nix-store --optimise to get rid of duplicate files.
'';
};
cores = mkOption {
type = types.int;
default = 0;
example = 64;
description = ''
This option defines the maximum number of concurrent tasks during
one build. It affects, e.g., -j option for make.
The special value 0 means that the builder should use all
available CPU cores in the system. Some builds may become
non-deterministic with this option; use with care! Packages will
only be affected if enableParallelBuilding is set for them.
'';
};
sandbox = mkOption {
type = types.either types.bool (types.enum [ "relaxed" ]);
default = true;
description = ''
If set, Nix will perform builds in a sandboxed environment that it
will set up automatically for each build. This prevents impurities
in builds by disallowing access to dependencies outside of the Nix
store by using network and mount namespaces in a chroot environment.
This is enabled by default even though it has a possible performance
impact due to the initial setup time of a sandbox for each build. It
doesn't affect derivation hashes, so changing this option will not
trigger a rebuild of packages.
'';
};
extra-sandbox-paths = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/dev" "/proc" ];
description = ''
Directories from the host filesystem to be included
in the sandbox.
'';
};
substituters = mkOption {
type = types.listOf types.str;
description = ''
List of binary cache URLs used to obtain pre-built binaries
of Nix packages.
By default https://cache.nixos.org/ is added.
'';
};
trusted-substituters = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "https://hydra.nixos.org/" ];
description = ''
List of binary cache URLs that non-root users can use (in
addition to those specified using
<option>nix.settings.substituters</option>) by passing
<literal>--option binary-caches</literal> to Nix commands.
'';
};
require-sigs = mkOption {
type = types.bool;
default = true;
description = ''
If enabled (the default), Nix will only download binaries from binary caches if
they are cryptographically signed with any of the keys listed in
<option>nix.settings.trusted-public-keys</option>. If disabled, signatures are neither
required nor checked, so it's strongly recommended that you use only
trustworthy caches and https to prevent man-in-the-middle attacks.
'';
};
trusted-public-keys = mkOption {
type = types.listOf types.str;
example = [ "hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=" ];
description = ''
List of public keys used to sign binary caches. If
<option>nix.settings.trusted-public-keys</option> is enabled,
then Nix will use a binary from a binary cache if and only
if it is signed by <emphasis>any</emphasis> of the keys
listed here. By default, only the key for
<uri>cache.nixos.org</uri> is included.
'';
};
trusted-users = mkOption {
type = types.listOf types.str;
default = [ "root" ];
example = [ "root" "alice" "@wheel" ];
description = ''
A list of names of users that have additional rights when
connecting to the Nix daemon, such as the ability to specify
additional binary caches, or to import unsigned NARs. You
can also specify groups by prefixing them with
<literal>@</literal>; for instance,
<literal>@wheel</literal> means all users in the wheel
group.
'';
};
system-features = mkOption {
type = types.listOf types.str;
example = [ "kvm" "big-parallel" "gccarch-skylake" ];
description = ''
The set of features supported by the machine. Derivations
can express dependencies on system features through the
<literal>requiredSystemFeatures</literal> attribute.
By default, pseudo-features <literal>nixos-test</literal>, <literal>benchmark</literal>,
and <literal>big-parallel</literal> used in Nixpkgs are set, <literal>kvm</literal>
is also included in it is avaliable.
'';
};
allowed-users = mkOption {
type = types.listOf types.str;
default = [ "*" ];
example = [ "@wheel" "@builders" "alice" "bob" ];
description = ''
A list of names of users (separated by whitespace) that are
allowed to connect to the Nix daemon. As with
<option>nix.settings.trusted-users</option>, you can specify groups by
prefixing them with <literal>@</literal>. Also, you can
allow all users by specifying <literal>*</literal>. The
default is <literal>*</literal>. Note that trusted users are
always allowed to connect.
'';
};
};
};
default = { };
example = literalExpression ''
{
use-sandbox = true;
show-trace = true;
system-features = [ "big-parallel" "kvm" "recursive-nix" ];
sandbox-paths = { "/bin/sh" = "''${pkgs.busybox-sandbox-shell.out}/bin/busybox"; };
}
'';
description = ''
Configuration for Nix, see
<link xlink:href="https://nixos.org/manual/nix/stable/#sec-conf-file"/> or
<citerefentry>
<refentrytitle>nix.conf</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry> for avalaible options.
The value declared here will be translated directly to the key-value pairs Nix expects.
</para>
<para>
You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.nix.settings</command>
to view the current value. By default it is empty.
</para>
<para>
Nix configurations defined under <option>nix.*</option> will be translated and applied to this
option. In addition, configuration specified in <option>nix.extraOptions</option> which will be appended
verbatim to the resulting config file.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
environment.systemPackages =
[
nixPackage
pkgs.nix-info
]
++ optional (config.programs.bash.enableCompletion) pkgs.nix-bash-completions;
environment.etc."nix/nix.conf".source = nixConf;
environment.etc."nix/registry.json".text = builtins.toJSON {
version = 2;
flakes = mapAttrsToList (n: v: { inherit (v) from to exact; }) cfg.registry;
};
# List of machines for distributed Nix builds in the format
# expected by build-remote.pl.
environment.etc."nix/machines" = mkIf (cfg.buildMachines != [ ]) {
text =
concatMapStrings
(machine:
(concatStringsSep " " ([
"${optionalString (machine.sshUser != null) "${machine.sshUser}@"}${machine.hostName}"
(if machine.system != null then machine.system else if machine.systems != [ ] then concatStringsSep "," machine.systems else "-")
(if machine.sshKey != null then machine.sshKey else "-")
(toString machine.maxJobs)
(toString machine.speedFactor)
(concatStringsSep "," (machine.supportedFeatures ++ machine.mandatoryFeatures))
(concatStringsSep "," machine.mandatoryFeatures)
]
++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
+ "\n"
)
cfg.buildMachines;
};
assertions =
let badMachine = m: m.system == null && m.systems == [ ];
in
[
{
assertion = !(any badMachine cfg.buildMachines);
message = ''
At least one system type (via <varname>system</varname> or
<varname>systems</varname>) must be set for every build machine.
Invalid machine specifications:
'' + " " +
(concatStringsSep "\n "
(map (m: m.hostName)
(filter (badMachine) cfg.buildMachines)));
}
];
systemd.packages = [ nixPackage ];
# Will only work once https://github.com/NixOS/nix/pull/6285 is merged
# systemd.tmpfiles.packages = [ nixPackage ];
# Can be dropped for Nix > https://github.com/NixOS/nix/pull/6285
systemd.tmpfiles.rules = [
"d /nix/var/nix/daemon-socket 0755 root root - -"
];
systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
systemd.services.nix-daemon =
{
path = [ nixPackage pkgs.util-linux config.programs.ssh.package ]
++ optionals cfg.distributedBuilds [ pkgs.gzip ];
environment = cfg.envVars
// { CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; }
// config.networking.proxy.envVars;
unitConfig.RequiresMountsFor = "/nix/store";
serviceConfig =
{
CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy;
IOSchedulingClass = cfg.daemonIOSchedClass;
IOSchedulingPriority = cfg.daemonIOSchedPriority;
LimitNOFILE = 1048576;
};
restartTriggers = [ nixConf ];
# `stopIfChanged = false` changes to switch behavior
# from stop -> update units -> start
# to update units -> restart
#
# The `stopIfChanged` setting therefore controls a trade-off between a
# more predictable lifecycle, which runs the correct "version" of
# the `ExecStop` line, and on the other hand the availability of
# sockets during the switch, as the effectiveness of the stop operation
# depends on the socket being stopped as well.
#
# As `nix-daemon.service` does not make use of `ExecStop`, we prefer
# to keep the socket up and available. This is important for machines
# that run Nix-based services, such as automated build, test, and deploy
# services, that expect the daemon socket to be available at all times.
#
# Notably, the Nix client does not retry on failure to connect to the
# daemon socket, and the in-process RemoteStore instance will disable
# itself. This makes retries infeasible even for services that are
# aware of the issue. Failure to connect can affect not only new client
# processes, but also new RemoteStore instances in existing processes,
# as well as existing RemoteStore instances that have not saturated
# their connection pool.
#
# Also note that `stopIfChanged = true` does not kill existing
# connection handling daemons, as one might wish to happen before a
# breaking Nix upgrade (which is rare). The daemon forks that handle
# the individual connections split off into their own sessions, causing
# them not to be stopped by systemd.
# If a Nix upgrade does require all existing daemon processes to stop,
# nix-daemon must do so on its own accord, and only when the new version
# starts and detects that Nix's persistent state needs an upgrade.
stopIfChanged = false;
};
# Set up the environment variables for running Nix.
environment.sessionVariables = cfg.envVars // { NIX_PATH = cfg.nixPath; };
environment.extraInit =
''
if [ -e "$HOME/.nix-defexpr/channels" ]; then
export NIX_PATH="$HOME/.nix-defexpr/channels''${NIX_PATH:+:$NIX_PATH}"
fi
'';
nix.nrBuildUsers = mkDefault (max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs));
users.users = nixbldUsers;
services.xserver.displayManager.hiddenUsers = attrNames nixbldUsers;
system.activationScripts.nix = stringAfter [ "etc" "users" ]
''
install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user
# Subscribe the root user to the NixOS channel by default.
if [ ! -e "/root/.nix-channels" ]; then
echo "${config.system.defaultChannel} nixos" > "/root/.nix-channels"
fi
'';
# Legacy configuration conversion.
nix.settings = mkMerge [
{
trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
substituters = mkAfter [ "https://cache.nixos.org/" ];
system-features = mkDefault (
[ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
optionals (pkgs.hostPlatform ? gcc.arch) (
# a builder can run code for `gcc.arch` and inferior architectures
[ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
map (x: "gccarch-${x}") systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
)
);
}
(mkIf (!cfg.distributedBuilds) { builders = null; })
(mkIf (isNixAtLeast "2.3pre") { sandbox-fallback = false; })
];
};
}

View file

@ -0,0 +1,106 @@
{ config, lib, ... }:
with lib;
let
cfg = config.nix.gc;
in
{
###### interface
options = {
nix.gc = {
automatic = mkOption {
default = false;
type = types.bool;
description = "Automatically run the garbage collector at a specific time.";
};
dates = mkOption {
type = types.str;
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.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
randomizedDelaySec = mkOption {
default = "0";
type = types.str;
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
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>
'';
};
persistent = mkOption {
default = true;
type = 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 = mkOption {
default = "";
example = "--max-freed $((64 * 1024**3))";
type = types.str;
description = ''
Options given to <filename>nix-collect-garbage</filename> 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}";
startAt = optional cfg.automatic cfg.dates;
};
systemd.timers.nix-gc = lib.mkIf cfg.automatic {
timerConfig = {
RandomizedDelaySec = cfg.randomizedDelaySec;
Persistent = cfg.persistent;
};
};
};
}

View file

@ -0,0 +1,57 @@
{ config, lib, ... }:
with lib;
let
cfg = config.nix.optimise;
in
{
###### interface
options = {
nix.optimise = {
automatic = mkOption {
default = false;
type = types.bool;
description = "Automatically run the nix store optimiser at a specific time.";
};
dates = mkOption {
default = ["03:45"];
type = types.listOf types.str;
description = ''
Specification (in the format described by
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>) of the time at
which the optimiser will run.
'';
};
};
};
###### implementation
config = {
assertions = [
{
assertion = cfg.automatic -> config.nix.enable;
message = ''nix.optimise.automatic requires nix.enable'';
}
];
systemd.services.nix-optimise = lib.mkIf config.nix.enable
{ 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 = optionals cfg.automatic cfg.dates;
};
};
}

View file

@ -0,0 +1,69 @@
{ config, lib, ... }:
with lib;
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 = mkOption {
type = types.bool;
default = false;
description = "Whether to enable serving the Nix store as a remote store via SSH.";
};
write = mkOption {
type = types.bool;
default = false;
description = "Whether to enable writing to the Nix store as a remote store via SSH. Note: the sshServe user is named nix-ssh and is not a trusted-user. nix-ssh should be added to the <option>nix.settings.trusted-users</option> option in most use cases, such as allowing remote building of derivations.";
};
keys = mkOption {
type = types.listOf 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 = mkOption {
type = types.enum [ "ssh" "ssh-ng" ];
default = "ssh";
description = "The specific Nix-over-SSH protocol to use.";
};
};
};
config = mkIf cfg.enable {
users.users.nix-ssh = {
description = "Nix SSH store user";
isSystemUser = true;
group = "nix-ssh";
useDefaultShell = true;
};
users.groups.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,31 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.novacomd;
in {
options = {
services.novacomd = {
enable = mkEnableOption "Novacom service for connecting to WebOS devices";
};
};
config = 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 maintainers; [ dtzWill ];
}

View file

@ -0,0 +1,117 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.nzbget;
pkg = pkgs.nzbget;
stateDir = "/var/lib/nzbget";
configFile = "${stateDir}/nzbget.conf";
configOpts = concatStringsSep " " (mapAttrsToList (name: value: "-o ${name}=${escapeShellArg (toStr value)}") cfg.settings);
toStr = v:
if v == true then "yes"
else if v == false then "no"
else if isInt v then toString v
else v;
in
{
imports = [
(mkRemovedOptionModule [ "services" "misc" "nzbget" "configFile" ] "The configuration of nzbget is now managed by users through the web interface.")
(mkRemovedOptionModule [ "services" "misc" "nzbget" "dataDir" ] "The data directory for nzbget is now /var/lib/nzbget.")
(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 = mkEnableOption "NZBGet";
user = mkOption {
type = types.str;
default = "nzbget";
description = "User account under which NZBGet runs";
};
group = mkOption {
type = types.str;
default = "nzbget";
description = "Group under which NZBGet runs";
};
settings = mkOption {
type = with types; attrsOf (oneOf [ bool int str ]);
default = {};
description = ''
NZBGet configuration, passed via command line using switch -o. Refer to
<link xlink:href="https://github.com/nzbget/nzbget/blob/master/nzbget.conf"/>
for details on supported values.
'';
example = {
MainDir = "/data";
};
};
};
};
# implementation
config = 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 = "${pkg}/share/nzbget/nzbget.conf";
WebDir = "${pkg}/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 ${pkg}/share/nzbget/nzbget.conf ${configFile}
fi
'';
serviceConfig = {
StateDirectory = "nzbget";
StateDirectoryMode = "0750";
User = cfg.user;
Group = cfg.group;
UMask = "0002";
Restart = "on-failure";
ExecStart = "${pkg}/bin/nzbget --server --configfile ${stateDir}/nzbget.conf ${configOpts}";
ExecStop = "${pkg}/bin/nzbget --quit";
};
};
users.users = mkIf (cfg.user == "nzbget") {
nzbget = {
home = stateDir;
group = cfg.group;
uid = config.ids.uids.nzbget;
};
};
users.groups = mkIf (cfg.group == "nzbget") {
nzbget = {
gid = config.ids.gids.nzbget;
};
};
};
}

View file

@ -0,0 +1,78 @@
{ config, pkgs, lib, ... }:
with lib;
let cfg = config.services.nzbhydra2;
in {
options = {
services.nzbhydra2 = {
enable = mkEnableOption "NZBHydra2";
dataDir = mkOption {
type = types.str;
default = "/var/lib/nzbhydra2";
description = "The directory where NZBHydra2 stores its data files.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description =
"Open ports in the firewall for the NZBHydra2 web interface.";
};
package = mkOption {
type = types.package;
default = pkgs.nzbhydra2;
defaultText = literalExpression "pkgs.nzbhydra2";
description = "NZBHydra2 package to use.";
};
};
};
config = 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 = mkIf cfg.openFirewall { allowedTCPPorts = [ 5076 ]; };
users.users.nzbhydra2 = {
group = "nzbhydra2";
isSystemUser = true;
};
users.groups.nzbhydra2 = {};
};
}

View file

@ -0,0 +1,133 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.octoprint;
baseConfig = {
plugins.curalegacy.cura_engine = "${pkgs.curaengine_stable}/bin/CuraEngine";
server.host = cfg.host;
server.port = cfg.port;
webcam.ffmpeg = "${pkgs.ffmpeg.bin}/bin/ffmpeg";
};
fullConfig = recursiveUpdate cfg.extraConfig baseConfig;
cfgUpdate = pkgs.writeText "octoprint-config.yaml" (builtins.toJSON fullConfig);
pluginsEnv = package.python.withPackages (ps: [ps.octoprint] ++ (cfg.plugins ps));
package = pkgs.octoprint;
in
{
##### interface
options = {
services.octoprint = {
enable = mkEnableOption "OctoPrint, web interface for 3D printers";
host = mkOption {
type = types.str;
default = "0.0.0.0";
description = ''
Host to bind OctoPrint to.
'';
};
port = mkOption {
type = types.port;
default = 5000;
description = ''
Port to bind OctoPrint to.
'';
};
user = mkOption {
type = types.str;
default = "octoprint";
description = "User for the daemon.";
};
group = mkOption {
type = types.str;
default = "octoprint";
description = "Group for the daemon.";
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/octoprint";
description = "State directory of the daemon.";
};
plugins = mkOption {
type = types.functionTo (types.listOf types.package);
default = plugins: [];
defaultText = literalExpression "plugins: []";
example = literalExpression "plugins: with plugins; [ themeify stlviewer ]";
description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
};
extraConfig = mkOption {
type = types.attrs;
default = {};
description = "Extra options which are added to OctoPrint's YAML configuration file.";
};
};
};
##### implementation
config = mkIf cfg.enable {
users.users = optionalAttrs (cfg.user == "octoprint") {
octoprint = {
group = cfg.group;
uid = config.ids.uids.octoprint;
};
};
users.groups = optionalAttrs (cfg.group == "octoprint") {
octoprint.gid = config.ids.gids.octoprint;
};
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
];
systemd.services.octoprint = {
description = "OctoPrint, web interface for 3D printers";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [ pluginsEnv ];
preStart = ''
if [ -e "${cfg.stateDir}/config.yaml" ]; then
${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${cfgUpdate}" > "${cfg.stateDir}/config.yaml.tmp"
mv "${cfg.stateDir}/config.yaml.tmp" "${cfg.stateDir}/config.yaml"
else
cp "${cfgUpdate}" "${cfg.stateDir}/config.yaml"
chmod 600 "${cfg.stateDir}/config.yaml"
fi
'';
serviceConfig = {
ExecStart = "${pluginsEnv}/bin/octoprint serve -b ${cfg.stateDir}";
User = cfg.user;
Group = cfg.group;
SupplementaryGroups = [
"dialout"
];
};
};
};
}

View file

@ -0,0 +1,81 @@
{ config, pkgs, lib, ... }:
with lib;
let cfg = config.services.ombi;
in {
options = {
services.ombi = {
enable = mkEnableOption ''
Ombi.
Optionally see <link xlink:href="https://docs.ombi.app/info/reverse-proxy"/>
on how to set up a reverse proxy
'';
dataDir = mkOption {
type = types.str;
default = "/var/lib/ombi";
description = "The directory where Ombi stores its data files.";
};
port = mkOption {
type = types.port;
default = 5000;
description = "The port for the Ombi web interface.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the Ombi web interface.";
};
user = mkOption {
type = types.str;
default = "ombi";
description = "User account under which Ombi runs.";
};
group = mkOption {
type = types.str;
default = "ombi";
description = "Group under which Ombi runs.";
};
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
];
systemd.services.ombi = {
description = "Ombi";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${pkgs.ombi}/bin/Ombi --storage '${cfg.dataDir}' --host 'http://*:${toString cfg.port}'";
Restart = "on-failure";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
users.users = mkIf (cfg.user == "ombi") {
ombi = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
};
};
users.groups = mkIf (cfg.group == "ombi") { ombi = { }; };
};
}

View file

@ -0,0 +1,86 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.osrm;
in
{
options.services.osrm = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable the OSRM service.";
};
address = mkOption {
type = types.str;
default = "0.0.0.0";
description = "IP address on which the web server will listen.";
};
port = mkOption {
type = types.int;
default = 5000;
description = "Port on which the web server will run.";
};
threads = mkOption {
type = types.int;
default = 4;
description = "Number of threads to use.";
};
algorithm = mkOption {
type = types.enum [ "CH" "CoreCH" "MLD" ];
default = "MLD";
description = "Algorithm to use for the data. Must be one of CH, CoreCH, MLD";
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [];
example = [ "--max-table-size 1000" "--max-matching-size 1000" ];
description = "Extra command line arguments passed to osrm-routed";
};
dataFile = mkOption {
type = types.path;
example = "/var/lib/osrm/berlin-latest.osrm";
description = "Data file location";
};
};
config = mkIf cfg.enable {
users.users.osrm = {
group = config.users.users.osrm.name;
description = "OSRM user";
createHome = false;
isSystemUser = true;
};
users.groups.osrm = { };
systemd.services.osrm = {
description = "OSRM service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = config.users.users.osrm.name;
ExecStart = ''
${pkgs.osrm-backend}/bin/osrm-routed \
--ip ${cfg.address} \
--port ${toString cfg.port} \
--threads ${toString cfg.threads} \
--algorithm ${cfg.algorithm} \
${toString cfg.extraFlags} \
${cfg.dataFile}
'';
};
};
};
}

View file

@ -0,0 +1,98 @@
{ lib, pkgs, config, ... }:
with lib;
let cfg = config.services.owncast;
in {
options.services.owncast = {
enable = mkEnableOption "owncast";
dataDir = mkOption {
type = types.str;
default = "/var/lib/owncast";
description = ''
The directory where owncast stores its data files. If left as the default value this directory will automatically be created before the owncast server starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open the appropriate ports in the firewall for owncast.
'';
};
user = mkOption {
type = types.str;
default = "owncast";
description = "User account under which owncast runs.";
};
group = mkOption {
type = types.str;
default = "owncast";
description = "Group under which owncast runs.";
};
listen = mkOption {
type = types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "The IP address to bind the owncast web server to.";
};
port = mkOption {
type = types.port;
default = 8080;
description = ''
TCP port where owncast web-gui listens.
'';
};
rtmp-port = mkOption {
type = types.port;
default = 1935;
description = ''
TCP port where owncast rtmp service listens.
'';
};
};
config = mkIf cfg.enable {
systemd.services.owncast = {
description = "A self-hosted live video and web chat server";
wantedBy = [ "multi-user.target" ];
serviceConfig = mkMerge [
{
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.dataDir;
ExecStart = "${pkgs.owncast}/bin/owncast -webserverport ${toString cfg.port} -rtmpport ${toString cfg.rtmp-port} -webserverip ${cfg.listen}";
Restart = "on-failure";
}
(mkIf (cfg.dataDir == "/var/lib/owncast") {
StateDirectory = "owncast";
})
];
};
users.users = mkIf (cfg.user == "owncast") {
owncast = {
isSystemUser = true;
group = cfg.group;
description = "owncast system user";
};
};
users.groups = mkIf (cfg.group == "owncast") { owncast = { }; };
networking.firewall =
mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.rtmp-port ] ++ optional (cfg.listen != "127.0.0.1") cfg.port; };
};
meta = { maintainers = with lib.maintainers; [ MayNiklas ]; };
}

View file

@ -0,0 +1,74 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.packagekit;
inherit (lib)
mkEnableOption mkOption mkIf mkRemovedOptionModule types
listToAttrs recursiveUpdate;
iniFmt = pkgs.formats.ini { };
confFiles = [
(iniFmt.generate "PackageKit.conf" (recursiveUpdate
{
Daemon = {
DefaultBackend = "nix";
KeepCache = false;
};
}
cfg.settings))
(iniFmt.generate "Vendor.conf" (recursiveUpdate
{
PackagesNotFound = rec {
DefaultUrl = "https://github.com/NixOS/nixpkgs";
CodecUrl = DefaultUrl;
HardwareUrl = DefaultUrl;
FontUrl = DefaultUrl;
MimeUrl = DefaultUrl;
};
}
cfg.vendorSettings))
];
in
{
imports = [
(mkRemovedOptionModule [ "services" "packagekit" "backend" ] "Always set to Nix.")
];
options.services.packagekit = {
enable = mkEnableOption ''
PackageKit provides a cross-platform D-Bus abstraction layer for
installing software. Software utilizing PackageKit can install
software regardless of the package manager.
'';
settings = mkOption {
type = iniFmt.type;
default = { };
description = "Additional settings passed straight through to PackageKit.conf";
};
vendorSettings = mkOption {
type = iniFmt.type;
default = { };
description = "Additional settings passed straight through to Vendor.conf";
};
};
config = mkIf cfg.enable {
services.dbus.packages = with pkgs; [ packagekit ];
environment.systemPackages = with pkgs; [ packagekit ];
systemd.packages = with pkgs; [ packagekit ];
environment.etc = listToAttrs (map
(e:
lib.nameValuePair "PackageKit/${e.name}" { source = e; })
confFiles);
};
}

View file

@ -0,0 +1,320 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.paperless;
defaultUser = "paperless";
# Don't start a redis instance if the user sets a custom redis connection
enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
redisServer = config.services.redis.servers.paperless;
env = {
PAPERLESS_DATA_DIR = cfg.dataDir;
PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
} // (
lib.mapAttrs (_: toString) cfg.extraConfig
) // (optionalAttrs enableRedis {
PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
});
manage = let
setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
in pkgs.writeShellScript "manage" ''
${setupEnv}
exec ${cfg.package}/bin/paperless-ngx "$@"
'';
# Secure the services
defaultServiceConfig = {
TemporaryFileSystem = "/:ro";
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
"-/run/postgresql"
] ++ (optional enableRedis redisServer.unixSocket);
BindPaths = [
cfg.consumptionDir
cfg.dataDir
cfg.mediaDir
];
CapabilityBoundingSet = "";
# ProtectClock adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
# Breaks if the home dir of the user is in /home
# Also does not add much value in combination with the TemporaryFileSystem.
# ProtectHome = true;
ProtectHostname = true;
# Would re-mount paths ignored by temporary root
#ProtectSystem = "strict";
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
# Don't restrict ProcSubset because django-q requires read access to /proc/stat
# to query CPU and memory information.
# Note that /proc only contains processes of user `paperless`, so this is safe.
# ProcSubset = "pid";
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SupplementaryGroups = optional enableRedis redisServer.user;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
# Does not work well with the temporary root
#UMask = "0066";
};
in
{
meta.maintainers = with maintainers; [ earvstedt Flakebi ];
imports = [
(mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
];
options.services.paperless = {
enable = mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable Paperless.
When started, the Paperless database is automatically created if it doesn't
exist and updated if the Paperless package has changed.
Both tasks are achieved by running a Django migration.
A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
<literal>''${dataDir}/paperless-manage</literal>.
'';
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/paperless";
description = "Directory to store the Paperless data.";
};
mediaDir = mkOption {
type = types.str;
default = "${cfg.dataDir}/media";
defaultText = literalExpression ''"''${dataDir}/media"'';
description = "Directory to store the Paperless documents.";
};
consumptionDir = mkOption {
type = types.str;
default = "${cfg.dataDir}/consume";
defaultText = literalExpression ''"''${dataDir}/consume"'';
description = "Directory from which new documents are imported.";
};
consumptionDirIsPublic = mkOption {
type = types.bool;
default = false;
description = "Whether all users can write to the consumption dir.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/paperless-password";
description = ''
A file containing the superuser password.
A superuser is required to access the web interface.
If unset, you can create a superuser manually by running
<literal>''${dataDir}/paperless-manage createsuperuser</literal>.
The default superuser name is <literal>admin</literal>. To change it, set
option <option>extraConfig.PAPERLESS_ADMIN_USER</option>.
WARNING: When changing the superuser name after the initial setup, the old superuser
will continue to exist.
To disable login for the web interface, set the following:
<literal>extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";</literal>.
WARNING: Only use this on a trusted system without internet access to Paperless.
'';
};
address = mkOption {
type = types.str;
default = "localhost";
description = "Web interface address.";
};
port = mkOption {
type = types.port;
default = 28981;
description = "Web interface port.";
};
extraConfig = mkOption {
type = types.attrs;
default = {};
description = ''
Extra paperless config options.
See <link xlink:href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html">the documentation</link>
for available options.
'';
example = literalExpression ''
{
PAPERLESS_OCR_LANGUAGE = "deu+eng";
}
'';
};
user = mkOption {
type = types.str;
default = defaultUser;
description = "User under which Paperless runs.";
};
package = mkOption {
type = types.package;
default = pkgs.paperless-ngx;
defaultText = literalExpression "pkgs.paperless-ngx";
description = "The Paperless package to use.";
};
};
config = mkIf cfg.enable {
services.redis.servers.paperless.enable = mkIf enableRedis true;
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
"d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
(if cfg.consumptionDirIsPublic then
"d '${cfg.consumptionDir}' 777 - - - -"
else
"d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
)
];
systemd.services.paperless-scheduler = {
description = "Paperless scheduler";
serviceConfig = defaultServiceConfig // {
User = cfg.user;
ExecStart = "${cfg.package}/bin/paperless-ngx qcluster";
Restart = "on-failure";
# The `mbind` syscall is needed for running the classifier.
SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
# Needs to talk to mail server for automated import rules
PrivateNetwork = false;
};
environment = env;
wantedBy = [ "multi-user.target" ];
wants = [ "paperless-consumer.service" "paperless-web.service" ];
preStart = ''
ln -sf ${manage} ${cfg.dataDir}/paperless-manage
# 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
${cfg.package}/bin/paperless-ngx migrate
echo ${cfg.package} > "$versionFile"
fi
''
+ optionalString (cfg.passwordFile != null) ''
export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
superuserStateFile="${cfg.dataDir}/superuser-state"
if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
${cfg.package}/bin/paperless-ngx manage_superuser
echo "$superuserState" > "$superuserStateFile"
fi
'';
} // optionalAttrs enableRedis {
after = [ "redis-paperless.service" ];
};
# Reading the user-provided password file requires root access
systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
requiredBy = [ "paperless-scheduler.service" ];
before = [ "paperless-scheduler.service" ];
serviceConfig = {
ExecStart = ''
${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
'${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
'';
Type = "oneshot";
};
};
systemd.services.paperless-consumer = {
description = "Paperless document consumer";
serviceConfig = defaultServiceConfig // {
User = cfg.user;
ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
Restart = "on-failure";
};
environment = env;
# Bind to `paperless-scheduler` so that the consumer never runs
# during migrations
bindsTo = [ "paperless-scheduler.service" ];
after = [ "paperless-scheduler.service" ];
};
systemd.services.paperless-web = {
description = "Paperless web server";
serviceConfig = defaultServiceConfig // {
User = cfg.user;
ExecStart = ''
${pkgs.python3Packages.gunicorn}/bin/gunicorn \
-c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
'';
Restart = "on-failure";
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
# gunicorn needs setuid
SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
# Needs to serve web page
PrivateNetwork = false;
};
environment = env // {
PATH = mkForce cfg.package.path;
PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ngx/src";
};
# Allow the web interface to access the private /tmp directory of the server.
# This is required to support uploading files via the web interface.
unitConfig.JoinsNamespaceOf = "paperless-scheduler.service";
# Bind to `paperless-scheduler` so that the web server never runs
# during migrations
bindsTo = [ "paperless-scheduler.service" ];
after = [ "paperless-scheduler.service" ];
};
users = optionalAttrs (cfg.user == defaultUser) {
users.${defaultUser} = {
group = defaultUser;
uid = config.ids.uids.paperless;
home = cfg.dataDir;
};
groups.${defaultUser} = {
gid = config.ids.gids.paperless;
};
};
};
}

View file

@ -0,0 +1,129 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.parsoid;
parsoid = pkgs.nodePackages.parsoid;
confTree = {
worker_heartbeat_timeout = 300000;
logging = { level = "info"; };
services = [{
module = "lib/index.js";
entrypoint = "apiServiceWorker";
conf = {
mwApis = map (x: if isAttrs x then x else { uri = x; }) cfg.wikis;
serverInterface = cfg.interface;
serverPort = cfg.port;
};
}];
};
confFile = pkgs.writeText "config.yml" (builtins.toJSON (recursiveUpdate confTree cfg.extraConfig));
in
{
imports = [
(mkRemovedOptionModule [ "services" "parsoid" "interwikis" ] "Use services.parsoid.wikis instead")
];
##### interface
options = {
services.parsoid = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Parsoid -- bidirectional
wikitext parser.
'';
};
wikis = mkOption {
type = types.listOf (types.either types.str types.attrs);
example = [ "http://localhost/api.php" ];
description = ''
Used MediaWiki API endpoints.
'';
};
workers = mkOption {
type = types.int;
default = 2;
description = ''
Number of Parsoid workers.
'';
};
interface = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Interface to listen on.
'';
};
port = mkOption {
type = types.int;
default = 8000;
description = ''
Port to listen on.
'';
};
extraConfig = mkOption {
type = types.attrs;
default = {};
description = ''
Extra configuration to add to parsoid configuration.
'';
};
};
};
##### implementation
config = mkIf cfg.enable {
systemd.services.parsoid = {
description = "Bidirectional wikitext parser";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${parsoid}/lib/node_modules/parsoid/bin/server.js -c ${confFile} -n ${toString cfg.workers}";
DynamicUser = true;
User = "parsoid";
Group = "parsoid";
CapabilityBoundingSet = "";
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
LockPersonality = true;
#MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
};
};
};
}

View file

@ -0,0 +1,60 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.persistent-evdev;
settingsFormat = pkgs.formats.json {};
configFile = settingsFormat.generate "persistent-evdev-config" {
cache = "/var/cache/persistent-evdev";
devices = lib.mapAttrs (virt: phys: "/dev/input/by-id/${phys}") cfg.devices;
};
in
{
options.services.persistent-evdev = {
enable = lib.mkEnableOption "virtual input devices that persist even if the backing device is hotplugged";
devices = lib.mkOption {
default = {};
type = with lib.types; attrsOf str;
description = ''
A set of virtual proxy device labels with backing physical device ids.
Physical devices should already exist in <filename class="devicefile">/dev/input/by-id/</filename>.
Proxy devices will be automatically given a <literal>uinput-</literal> prefix.
See the <link xlink:href="https://github.com/aiberia/persistent-evdev#example-usage-with-libvirt">
project page</link> for example configuration of virtual devices with libvirt
and remember to add <literal>uinput-*</literal> devices to the qemu
<literal>cgroup_device_acl</literal> list (see <xref linkend="opt-virtualisation.libvirtd.qemu.verbatimConfig"/>).
'';
example = lib.literalExpression ''
{
persist-mouse0 = "usb-Logitech_G403_Prodigy_Gaming_Mouse_078738533531-event-if01";
persist-mouse1 = "usb-Logitech_G403_Prodigy_Gaming_Mouse_078738533531-event-mouse";
persist-mouse2 = "usb-Logitech_G403_Prodigy_Gaming_Mouse_078738533531-if01-event-kbd";
persist-keyboard0 = "usb-Microsoft_Natural®_Ergonomic_Keyboard_4000-event-kbd";
persist-keyboard1 = "usb-Microsoft_Natural®_Ergonomic_Keyboard_4000-if01-event-kbd";
}
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.persistent-evdev = {
documentation = [ "https://github.com/aiberia/persistent-evdev/blob/master/README.md" ];
description = "Persistent evdev proxy";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "on-failure";
ExecStart = "${pkgs.persistent-evdev}/bin/persistent-evdev.py ${configFile}";
CacheDirectory = "persistent-evdev";
};
};
services.udev.packages = [ pkgs.persistent-evdev ];
};
meta.maintainers = with lib.maintainers; [ lodi ];
}

View file

@ -0,0 +1,103 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.pinnwand;
format = pkgs.formats.toml {};
configFile = format.generate "pinnwand.toml" cfg.settings;
in
{
options.services.pinnwand = {
enable = mkEnableOption "Pinnwand";
port = mkOption {
type = types.port;
description = "The port to listen on.";
default = 8000;
};
settings = mkOption {
type = format.type;
description = ''
Your <filename>pinnwand.toml</filename> as a Nix attribute set. Look up
possible options in the <link xlink:href="https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example">pinnwand.toml-example</link>.
'';
default = {};
};
};
config = mkIf cfg.enable {
services.pinnwand.settings = {
database_uri = mkDefault "sqlite:////var/lib/pinnwand/pinnwand.db";
paste_size = mkDefault 262144;
paste_help = mkDefault ''
<p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
'';
footer = mkDefault ''
View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
'';
};
systemd.services = let
hardeningOptions = {
User = "pinnwand";
DynamicUser = true;
StateDirectory = "pinnwand";
StateDirectoryMode = "0700";
AmbientCapabilities = [];
CapabilityBoundingSet = "";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateDevices = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "0077";
};
command = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile}";
in {
pinnwand = {
description = "Pinnwannd HTTP Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
serviceConfig = {
ExecStart = "${command} http --port ${toString(cfg.port)}";
} // hardeningOptions;
};
pinnwand-reaper = {
description = "Pinnwand Reaper";
startAt = "daily";
serviceConfig = {
ExecStart = "${command} -vvvv reap"; # verbosity increased to show number of deleted pastes
} // hardeningOptions;
};
};
};
}

View file

@ -0,0 +1,180 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.plex;
in
{
imports = [
(mkRemovedOptionModule [ "services" "plex" "managePlugins" ] "Please omit or define the option: `services.plex.extraPlugins' instead.")
];
options = {
services.plex = {
enable = mkEnableOption "Plex Media Server";
dataDir = mkOption {
type = types.str;
default = "/var/lib/plex";
description = ''
The directory where Plex stores its data files.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for the media server.
'';
};
user = mkOption {
type = types.str;
default = "plex";
description = ''
User account under which Plex runs.
'';
};
group = mkOption {
type = types.str;
default = "plex";
description = ''
Group under which Plex runs.
'';
};
extraPlugins = mkOption {
type = types.listOf types.path;
default = [];
description = ''
A list of paths to extra plugin bundles to install in Plex's plugin
directory. Every time the systemd unit for Plex starts up, all of the
symlinks in Plex's plugin directory will be cleared and this module
will symlink all of the paths specified here to that directory.
'';
example = literalExpression ''
[
(builtins.path {
name = "Audnexus.bundle";
path = pkgs.fetchFromGitHub {
owner = "djdembeck";
repo = "Audnexus.bundle";
rev = "v0.2.8";
sha256 = "sha256-IWOSz3vYL7zhdHan468xNc6C/eQ2C2BukQlaJNLXh7E=";
};
})
]
'';
};
extraScanners = mkOption {
type = types.listOf types.path;
default = [];
description = ''
A list of paths to extra scanners to install in Plex's scanners
directory.
Every time the systemd unit for Plex starts up, all of the symlinks
in Plex's scanners directory will be cleared and this module will
symlink all of the paths specified here to that directory.
'';
example = literalExpression ''
[
(fetchFromGitHub {
owner = "ZeroQI";
repo = "Absolute-Series-Scanner";
rev = "773a39f502a1204b0b0255903cee4ed02c46fde0";
sha256 = "4l+vpiDdC8L/EeJowUgYyB3JPNTZ1sauN8liFAcK+PY=";
})
]
'';
};
package = mkOption {
type = types.package;
default = pkgs.plex;
defaultText = literalExpression "pkgs.plex";
description = ''
The Plex package to use. Plex subscribers may wish to use their own
package here, pointing to subscriber-only server versions.
'';
};
};
};
config = mkIf cfg.enable {
# Most of this is just copied from the RPM package's systemd service file.
systemd.services.plex = {
description = "Plex Media Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
# Run the pre-start script with full permissions (the "!" prefix) so it
# can create the data directory if necessary.
ExecStartPre = let
preStartScript = pkgs.writeScript "plex-run-prestart" ''
#!${pkgs.bash}/bin/bash
# Create data directory if it doesn't exist
if ! test -d "$PLEX_DATADIR"; then
echo "Creating initial Plex data directory in: $PLEX_DATADIR"
install -d -m 0755 -o "${cfg.user}" -g "${cfg.group}" "$PLEX_DATADIR"
fi
'';
in
"!${preStartScript}";
ExecStart = "${cfg.package}/bin/plexmediaserver";
KillSignal = "SIGQUIT";
Restart = "on-failure";
};
environment = {
# Configuration for our FHS userenv script
PLEX_DATADIR=cfg.dataDir;
PLEX_PLUGINS=concatMapStringsSep ":" builtins.toString cfg.extraPlugins;
PLEX_SCANNERS=concatMapStringsSep ":" builtins.toString cfg.extraScanners;
# The following variables should be set by the FHS userenv script:
# PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR
# PLEX_MEDIA_SERVER_HOME
# Allow access to GPU acceleration; the Plex LD_LIBRARY_PATH is added
# by the FHS userenv script.
LD_LIBRARY_PATH="/run/opengl-driver/lib";
PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6";
PLEX_MEDIA_SERVER_TMPDIR="/tmp";
PLEX_MEDIA_SERVER_USE_SYSLOG="true";
LC_ALL="en_US.UTF-8";
LANG="en_US.UTF-8";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 32400 3005 8324 32469 ];
allowedUDPPorts = [ 1900 5353 32410 32412 32413 32414 ];
};
users.users = mkIf (cfg.user == "plex") {
plex = {
group = cfg.group;
uid = config.ids.uids.plex;
};
};
users.groups = mkIf (cfg.group == "plex") {
plex = {
gid = config.ids.gids.plex;
};
};
};
}

View file

@ -0,0 +1,82 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.plikd;
format = pkgs.formats.toml {};
plikdCfg = format.generate "plikd.cfg" cfg.settings;
in
{
options = {
services.plikd = {
enable = mkEnableOption "the plikd server";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the plikd.";
};
settings = mkOption {
type = format.type;
default = {};
description = ''
Configuration for plikd, see <link xlink:href="https://github.com/root-gg/plik/blob/master/server/plikd.cfg"/>
for supported values.
'';
};
};
};
config = mkIf cfg.enable {
services.plikd.settings = mapAttrs (name: mkDefault) {
ListenPort = 8080;
ListenAddress = "localhost";
DataBackend = "file";
DataBackendConfig = {
Directory = "/var/lib/plikd";
};
MetadataBackendConfig = {
Driver = "sqlite3";
ConnectionString = "/var/lib/plikd/plik.db";
};
};
systemd.services.plikd = {
description = "Plikd file sharing server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.plikd}/bin/plikd --config ${plikdCfg}";
Restart = "on-failure";
StateDirectory = "plikd";
LogsDirectory = "plikd";
DynamicUser = true;
# Basic hardening
NoNewPrivileges = "yes";
PrivateTmp = "yes";
PrivateDevices = "yes";
DevicePolicy = "closed";
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 = "yes";
LockPersonality = "yes";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.settings.ListenPort ];
};
};
}

View file

@ -0,0 +1,50 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.podgrab;
in
{
options.services.podgrab = with lib; {
enable = mkEnableOption "Podgrab, a self-hosted podcast manager";
passwordFile = mkOption {
type = with types; nullOr str;
default = null;
example = "/run/secrets/password.env";
description = ''
The path to a file containing the PASSWORD environment variable
definition for Podgrab's authentification.
'';
};
port = mkOption {
type = types.port;
default = 8080;
example = 4242;
description = "The port on which Podgrab will listen for incoming HTTP traffic.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.podgrab = {
description = "Podgrab podcast manager";
wantedBy = [ "multi-user.target" ];
environment = {
CONFIG = "/var/lib/podgrab/config";
DATA = "/var/lib/podgrab/data";
GIN_MODE = "release";
PORT = toString cfg.port;
};
serviceConfig = {
DynamicUser = true;
EnvironmentFile = lib.optional (cfg.passwordFile != null) [
cfg.passwordFile
];
ExecStart = "${pkgs.podgrab}/bin/podgrab";
WorkingDirectory = "${pkgs.podgrab}/share";
StateDirectory = [ "podgrab/config" "podgrab/data" ];
};
};
};
meta.maintainers = with lib.maintainers; [ ambroisie ];
}

View file

@ -0,0 +1,41 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.prowlarr;
in
{
options = {
services.prowlarr = {
enable = mkEnableOption "Prowlarr";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the Prowlarr web interface.";
};
};
};
config = mkIf cfg.enable {
systemd.services.prowlarr = {
description = "Prowlarr";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = "prowlarr";
ExecStart = "${pkgs.prowlarr}/bin/Prowlarr -nobrowser -data=/var/lib/prowlarr";
Restart = "on-failure";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 9696 ];
};
};
}

View file

@ -0,0 +1,92 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.pykms;
libDir = "/var/lib/pykms";
in
{
meta.maintainers = with lib.maintainers; [ peterhoeg ];
imports = [
(mkRemovedOptionModule [ "services" "pykms" "verbose" ] "Use services.pykms.logLevel instead")
];
options = {
services.pykms = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the PyKMS service.";
};
listenAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = "The IP address on which to listen.";
};
port = mkOption {
type = types.int;
default = 1688;
description = "The port on which to listen.";
};
openFirewallPort = mkOption {
type = types.bool;
default = false;
description = "Whether the listening port should be opened automatically.";
};
memoryLimit = mkOption {
type = types.str;
default = "64M";
description = "How much memory to use at most.";
};
logLevel = mkOption {
type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MININFO" ];
default = "INFO";
description = "How much to log";
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Additional arguments";
};
};
};
config = mkIf cfg.enable {
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewallPort [ cfg.port ];
systemd.services.pykms = {
description = "Python KMS";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# python programs with DynamicUser = true require HOME to be set
environment.HOME = libDir;
serviceConfig = with pkgs; {
DynamicUser = true;
StateDirectory = baseNameOf libDir;
ExecStartPre = "${getBin pykms}/libexec/create_pykms_db.sh ${libDir}/clients.db";
ExecStart = lib.concatStringsSep " " ([
"${getBin pykms}/bin/server"
"--logfile=STDOUT"
"--loglevel=${cfg.logLevel}"
"--sqlite=${libDir}/clients.db"
] ++ cfg.extraArgs ++ [
cfg.listenAddress
(toString cfg.port)
]);
ProtectHome = "tmpfs";
WorkingDirectory = libDir;
SyslogIdentifier = "pykms";
Restart = "on-failure";
MemoryLimit = cfg.memoryLimit;
};
};
};
}

View file

@ -0,0 +1,75 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.radarr;
in
{
options = {
services.radarr = {
enable = mkEnableOption "Radarr";
dataDir = mkOption {
type = types.str;
default = "/var/lib/radarr/.config/Radarr";
description = "The directory where Radarr stores its data files.";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the Radarr web interface.";
};
user = mkOption {
type = types.str;
default = "radarr";
description = "User account under which Radarr runs.";
};
group = mkOption {
type = types.str;
default = "radarr";
description = "Group under which Radarr runs.";
};
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
];
systemd.services.radarr = {
description = "Radarr";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${pkgs.radarr}/bin/Radarr -nobrowser -data='${cfg.dataDir}'";
Restart = "on-failure";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 7878 ];
};
users.users = mkIf (cfg.user == "radarr") {
radarr = {
group = cfg.group;
home = cfg.dataDir;
uid = config.ids.uids.radarr;
};
};
users.groups = mkIf (cfg.group == "radarr") {
radarr.gid = config.ids.gids.radarr;
};
};
}

View file

@ -0,0 +1,384 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types;
inherit (lib) concatStringsSep literalExpression mapAttrsToList;
inherit (lib) optional optionalAttrs optionalString;
cfg = config.services.redmine;
format = pkgs.formats.yaml {};
bundle = "${cfg.package}/share/redmine/bin/bundle";
databaseYml = pkgs.writeText "database.yml" ''
production:
adapter: ${cfg.database.type}
database: ${cfg.database.name}
host: ${if (cfg.database.type == "postgresql" && cfg.database.socket != null) then cfg.database.socket else cfg.database.host}
port: ${toString cfg.database.port}
username: ${cfg.database.user}
password: #dbpass#
${optionalString (cfg.database.type == "mysql2" && cfg.database.socket != null) "socket: ${cfg.database.socket}"}
'';
configurationYml = format.generate "configuration.yml" cfg.settings;
additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv;
unpackTheme = unpack "theme";
unpackPlugin = unpack "plugin";
unpack = id: (name: source:
pkgs.stdenv.mkDerivation {
name = "redmine-${id}-${name}";
nativeBuildInputs = [ pkgs.unzip ];
buildCommand = ''
mkdir -p $out
cd $out
unpackFile ${source}
'';
});
mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql2";
pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql";
in
{
imports = [
(mkRemovedOptionModule [ "services" "redmine" "extraConfig" ] "Use services.redmine.settings instead.")
(mkRemovedOptionModule [ "services" "redmine" "database" "password" ] "Use services.redmine.database.passwordFile instead.")
];
# interface
options = {
services.redmine = {
enable = mkEnableOption "Redmine";
package = mkOption {
type = types.package;
default = pkgs.redmine;
defaultText = literalExpression "pkgs.redmine";
description = "Which Redmine package to use.";
example = literalExpression "pkgs.redmine.override { ruby = pkgs.ruby_2_7; }";
};
user = mkOption {
type = types.str;
default = "redmine";
description = "User under which Redmine is ran.";
};
group = mkOption {
type = types.str;
default = "redmine";
description = "Group under which Redmine is ran.";
};
port = mkOption {
type = types.port;
default = 3000;
description = "Port on which Redmine is ran.";
};
stateDir = mkOption {
type = types.str;
default = "/var/lib/redmine";
description = "The state directory, logs and plugins are stored here.";
};
settings = mkOption {
type = format.type;
default = {};
description = ''
Redmine configuration (<filename>configuration.yml</filename>). Refer to
<link xlink:href="https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration"/>
for details.
'';
example = literalExpression ''
{
email_delivery = {
delivery_method = "smtp";
smtp_settings = {
address = "mail.example.com";
port = 25;
};
};
}
'';
};
extraEnv = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration in additional_environment.rb.
See <link xlink:href="https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example"/>
for details.
'';
example = ''
config.logger.level = Logger::DEBUG
'';
};
themes = mkOption {
type = types.attrsOf types.path;
default = {};
description = "Set of themes.";
example = literalExpression ''
{
dkuk-redmine_alex_skin = builtins.fetchurl {
url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip";
sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
};
}
'';
};
plugins = mkOption {
type = types.attrsOf types.path;
default = {};
description = "Set of plugins.";
example = literalExpression ''
{
redmine_env_auth = builtins.fetchurl {
url = "https://github.com/Intera/redmine_env_auth/archive/0.6.zip";
sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak";
};
}
'';
};
database = {
type = mkOption {
type = types.enum [ "mysql2" "postgresql" ];
example = "postgresql";
default = "mysql2";
description = "Database engine to use.";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "Database host address.";
};
port = mkOption {
type = types.int;
default = if cfg.database.type == "postgresql" then 5432 else 3306;
defaultText = literalExpression "3306";
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "redmine";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "redmine";
description = "Database user.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/redmine-dbpassword";
description = ''
A file containing the password corresponding to
<option>database.user</option>.
'';
};
socket = mkOption {
type = types.nullOr types.path;
default =
if mysqlLocal then "/run/mysqld/mysqld.sock"
else if pgsqlLocal then "/run/postgresql"
else null;
defaultText = literalExpression "/run/mysqld/mysqld.sock";
example = "/run/mysqld/mysqld.sock";
description = "Path to the unix socket file to use for authentication.";
};
createLocally = mkOption {
type = types.bool;
default = true;
description = "Create the database and database user locally.";
};
};
};
};
# implementation
config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.database.passwordFile != null || cfg.database.socket != null;
message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set";
}
{ assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true";
}
{ assertion = cfg.database.createLocally -> cfg.database.socket != null;
message = "services.redmine.database.socket must be set if services.redmine.database.createLocally is set to true";
}
{ assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
message = "services.redmine.database.host must be set to localhost if services.redmine.database.createLocally is set to true";
}
];
services.redmine.settings = {
production = {
scm_subversion_command = "${pkgs.subversion}/bin/svn";
scm_mercurial_command = "${pkgs.mercurial}/bin/hg";
scm_git_command = "${pkgs.git}/bin/git";
scm_cvs_command = "${pkgs.cvs}/bin/cvs";
scm_bazaar_command = "${pkgs.breezy}/bin/bzr";
scm_darcs_command = "${pkgs.darcs}/bin/darcs";
};
};
services.redmine.extraEnv = mkBefore ''
config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
config.logger.level = Logger::INFO
'';
services.mysql = mkIf mysqlLocal {
enable = true;
package = mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{ name = cfg.database.user;
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
}
];
};
services.postgresql = mkIf pgsqlLocal {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{ name = cfg.database.user;
ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
}
];
};
# create symlinks for the basic directory layout the redmine package expects
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/public/themes' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -"
"d /run/redmine - - - - -"
"d /run/redmine/public - - - - -"
"L+ /run/redmine/config - - - - ${cfg.stateDir}/config"
"L+ /run/redmine/files - - - - ${cfg.stateDir}/files"
"L+ /run/redmine/log - - - - ${cfg.stateDir}/log"
"L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins"
"L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets"
"L+ /run/redmine/public/themes - - - - ${cfg.stateDir}/public/themes"
"L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp"
];
systemd.services.redmine = {
after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
wantedBy = [ "multi-user.target" ];
environment.RAILS_ENV = "production";
environment.RAILS_CACHE = "${cfg.stateDir}/cache";
environment.REDMINE_LANG = "en";
environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
path = with pkgs; [
imagemagick
breezy
cvs
darcs
git
mercurial
subversion
];
preStart = ''
rm -rf "${cfg.stateDir}/plugins/"*
rm -rf "${cfg.stateDir}/public/themes/"*
# start with a fresh config directory
# the config directory is copied instead of linked as some mutable data is stored in there
find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} +
cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/"
chmod -R u+w "${cfg.stateDir}/config"
# link in the application configuration
ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml"
# link in the additional environment configuration
ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb"
# link in all user specified themes
for theme in ${concatStringsSep " " (mapAttrsToList unpackTheme cfg.themes)}; do
ln -fs $theme/* "${cfg.stateDir}/public/themes"
done
# link in redmine provided themes
ln -sf ${cfg.package}/share/redmine/public/themes.dist/* "${cfg.stateDir}/public/themes/"
# link in all user specified plugins
for plugin in ${concatStringsSep " " (mapAttrsToList unpackPlugin cfg.plugins)}; do
ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}"
done
# handle database.passwordFile & permissions
DBPASS=${optionalString (cfg.database.passwordFile != null) "$(head -n1 ${cfg.database.passwordFile})"}
cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml"
sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml"
chmod 440 "${cfg.stateDir}/config/database.yml"
# generate a secret token if required
if ! test -e "${cfg.stateDir}/config/initializers/secret_token.rb"; then
${bundle} exec rake generate_secret_token
chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb"
fi
# execute redmine required commands prior to starting the application
${bundle} exec rake db:migrate
${bundle} exec rake redmine:plugins:migrate
${bundle} exec rake redmine:load_default_data
'';
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
TimeoutSec = "300";
WorkingDirectory = "${cfg.package}/share/redmine";
ExecStart="${bundle} exec rails server webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'";
};
};
users.users = optionalAttrs (cfg.user == "redmine") {
redmine = {
group = cfg.group;
home = cfg.stateDir;
uid = config.ids.uids.redmine;
};
};
users.groups = optionalAttrs (cfg.group == "redmine") {
redmine.gid = config.ids.gids.redmine;
};
};
}

View file

@ -0,0 +1,195 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.rippleDataApi;
deployment_env_config = builtins.toJSON {
production = {
port = toString cfg.port;
maxSockets = 150;
batchSize = 100;
startIndex = 32570;
rippleds = cfg.rippleds;
redis = {
enable = cfg.redis.enable;
host = cfg.redis.host;
port = cfg.redis.port;
options.auth_pass = null;
};
};
};
db_config = builtins.toJSON {
production = {
username = optional (cfg.couchdb.pass != "") cfg.couchdb.user;
password = optional (cfg.couchdb.pass != "") cfg.couchdb.pass;
host = cfg.couchdb.host;
port = cfg.couchdb.port;
database = cfg.couchdb.db;
protocol = "http";
};
};
in {
options = {
services.rippleDataApi = {
enable = mkEnableOption "ripple data api";
port = mkOption {
description = "Ripple data api port";
default = 5993;
type = types.int;
};
importMode = mkOption {
description = "Ripple data api import mode.";
default = "liveOnly";
type = types.enum ["live" "liveOnly"];
};
minLedger = mkOption {
description = "Ripple data api minimal ledger to fetch.";
default = null;
type = types.nullOr types.int;
};
maxLedger = mkOption {
description = "Ripple data api maximal ledger to fetch.";
default = null;
type = types.nullOr types.int;
};
redis = {
enable = mkOption {
description = "Whether to enable caching of ripple data to redis.";
default = true;
type = types.bool;
};
host = mkOption {
description = "Ripple data api redis host.";
default = "localhost";
type = types.str;
};
port = mkOption {
description = "Ripple data api redis port.";
default = 5984;
type = types.int;
};
};
couchdb = {
host = mkOption {
description = "Ripple data api couchdb host.";
default = "localhost";
type = types.str;
};
port = mkOption {
description = "Ripple data api couchdb port.";
default = 5984;
type = types.int;
};
db = mkOption {
description = "Ripple data api couchdb database.";
default = "rippled";
type = types.str;
};
user = mkOption {
description = "Ripple data api couchdb username.";
default = "rippled";
type = types.str;
};
pass = mkOption {
description = "Ripple data api couchdb password.";
default = "";
type = types.str;
};
create = mkOption {
description = "Whether to create couchdb database needed by ripple data api.";
type = types.bool;
default = true;
};
};
rippleds = mkOption {
description = "List of rippleds to be used by ripple data api.";
default = [
"http://s_east.ripple.com:51234"
"http://s_west.ripple.com:51234"
];
type = types.listOf types.str;
};
};
};
config = mkIf (cfg.enable) {
services.couchdb.enable = mkDefault true;
services.couchdb.bindAddress = mkDefault "0.0.0.0";
services.redis.enable = mkDefault true;
systemd.services.ripple-data-api = {
after = [ "couchdb.service" "redis.service" "ripple-data-api-importer.service" ];
wantedBy = [ "multi-user.target" ];
environment = {
NODE_ENV = "production";
DEPLOYMENT_ENVS_CONFIG = pkgs.writeText "deployment.environment.json" deployment_env_config;
DB_CONFIG = pkgs.writeText "db.config.json" db_config;
};
serviceConfig = {
ExecStart = "${pkgs.ripple-data-api}/bin/api";
Restart = "always";
User = "ripple-data-api";
};
};
systemd.services.ripple-data-importer = {
after = [ "couchdb.service" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.curl ];
environment = {
NODE_ENV = "production";
DEPLOYMENT_ENVS_CONFIG = pkgs.writeText "deployment.environment.json" deployment_env_config;
DB_CONFIG = pkgs.writeText "db.config.json" db_config;
LOG_FILE = "/dev/null";
};
serviceConfig = let
importMode =
if cfg.minLedger != null && cfg.maxLedger != null then
"${toString cfg.minLedger} ${toString cfg.maxLedger}"
else
cfg.importMode;
in {
ExecStart = "${pkgs.ripple-data-api}/bin/importer ${importMode} debug";
Restart = "always";
User = "ripple-data-api";
};
preStart = mkMerge [
(mkIf (cfg.couchdb.create) ''
HOST="http://${optionalString (cfg.couchdb.pass != "") "${cfg.couchdb.user}:${cfg.couchdb.pass}@"}${cfg.couchdb.host}:${toString cfg.couchdb.port}"
curl -X PUT $HOST/${cfg.couchdb.db} || true
'')
"${pkgs.ripple-data-api}/bin/update-views"
];
};
users.users.ripple-data-api =
{ description = "Ripple data api user";
isSystemUser = true;
group = "ripple-data-api";
};
users.groups.ripple-data-api = {};
};
}

View file

@ -0,0 +1,438 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.rippled;
opt = options.services.rippled;
b2i = val: if val then "1" else "0";
dbCfg = db: ''
type=${db.type}
path=${db.path}
${optionalString (db.compression != null) ("compression=${b2i db.compression}") }
${optionalString (db.onlineDelete != null) ("online_delete=${toString db.onlineDelete}")}
${optionalString (db.advisoryDelete != null) ("advisory_delete=${b2i db.advisoryDelete}")}
${db.extraOpts}
'';
rippledCfg = ''
[server]
${concatMapStringsSep "\n" (n: "port_${n}") (attrNames cfg.ports)}
${concatMapStrings (p: ''
[port_${p.name}]
ip=${p.ip}
port=${toString p.port}
protocol=${concatStringsSep "," p.protocol}
${optionalString (p.user != "") "user=${p.user}"}
${optionalString (p.password != "") "user=${p.password}"}
admin=${concatStringsSep "," p.admin}
${optionalString (p.ssl.key != null) "ssl_key=${p.ssl.key}"}
${optionalString (p.ssl.cert != null) "ssl_cert=${p.ssl.cert}"}
${optionalString (p.ssl.chain != null) "ssl_chain=${p.ssl.chain}"}
'') (attrValues cfg.ports)}
[database_path]
${cfg.databasePath}
[node_db]
${dbCfg cfg.nodeDb}
${optionalString (cfg.tempDb != null) ''
[temp_db]
${dbCfg cfg.tempDb}''}
${optionalString (cfg.importDb != null) ''
[import_db]
${dbCfg cfg.importDb}''}
[ips]
${concatStringsSep "\n" cfg.ips}
[ips_fixed]
${concatStringsSep "\n" cfg.ipsFixed}
[validators]
${concatStringsSep "\n" cfg.validators}
[node_size]
${cfg.nodeSize}
[ledger_history]
${toString cfg.ledgerHistory}
[fetch_depth]
${toString cfg.fetchDepth}
[validation_quorum]
${toString cfg.validationQuorum}
[sntp_servers]
${concatStringsSep "\n" cfg.sntpServers}
${optionalString cfg.statsd.enable ''
[insight]
server=statsd
address=${cfg.statsd.address}
prefix=${cfg.statsd.prefix}
''}
[rpc_startup]
{ "command": "log_level", "severity": "${cfg.logLevel}" }
'' + cfg.extraConfig;
portOptions = { name, ...}: {
options = {
name = mkOption {
internal = true;
default = name;
};
ip = mkOption {
default = "127.0.0.1";
description = "Ip where rippled listens.";
type = types.str;
};
port = mkOption {
description = "Port where rippled listens.";
type = types.int;
};
protocol = mkOption {
description = "Protocols expose by rippled.";
type = types.listOf (types.enum ["http" "https" "ws" "wss" "peer"]);
};
user = mkOption {
description = "When set, these credentials will be required on HTTP/S requests.";
type = types.str;
default = "";
};
password = mkOption {
description = "When set, these credentials will be required on HTTP/S requests.";
type = types.str;
default = "";
};
admin = mkOption {
description = "A comma-separated list of admin IP addresses.";
type = types.listOf types.str;
default = ["127.0.0.1"];
};
ssl = {
key = mkOption {
description = ''
Specifies the filename holding the SSL key in PEM format.
'';
default = null;
type = types.nullOr types.path;
};
cert = mkOption {
description = ''
Specifies the path to the SSL certificate file in PEM format.
This is not needed if the chain includes it.
'';
default = null;
type = types.nullOr types.path;
};
chain = mkOption {
description = ''
If you need a certificate chain, specify the path to the
certificate chain here. The chain may include the end certificate.
'';
default = null;
type = types.nullOr types.path;
};
};
};
};
dbOptions = {
options = {
type = mkOption {
description = "Rippled database type.";
type = types.enum ["rocksdb" "nudb"];
default = "rocksdb";
};
path = mkOption {
description = "Location to store the database.";
type = types.path;
default = cfg.databasePath;
defaultText = literalExpression "config.${opt.databasePath}";
};
compression = mkOption {
description = "Whether to enable snappy compression.";
type = types.nullOr types.bool;
default = null;
};
onlineDelete = mkOption {
description = "Enable automatic purging of older ledger information.";
type = types.nullOr (types.addCheck types.int (v: v > 256));
default = cfg.ledgerHistory;
defaultText = literalExpression "config.${opt.ledgerHistory}";
};
advisoryDelete = mkOption {
description = ''
If set, then require administrative RPC call "can_delete"
to enable online deletion of ledger records.
'';
type = types.nullOr types.bool;
default = null;
};
extraOpts = mkOption {
description = "Extra database options.";
type = types.lines;
default = "";
};
};
};
in
{
###### interface
options = {
services.rippled = {
enable = mkEnableOption "rippled";
package = mkOption {
description = "Which rippled package to use.";
type = types.package;
default = pkgs.rippled;
defaultText = literalExpression "pkgs.rippled";
};
ports = mkOption {
description = "Ports exposed by rippled";
type = with types; attrsOf (submodule portOptions);
default = {
rpc = {
port = 5005;
admin = ["127.0.0.1"];
protocol = ["http"];
};
peer = {
port = 51235;
ip = "0.0.0.0";
protocol = ["peer"];
};
ws_public = {
port = 5006;
ip = "0.0.0.0";
protocol = ["ws" "wss"];
};
};
};
nodeDb = mkOption {
description = "Rippled main database options.";
type = with types; nullOr (submodule dbOptions);
default = {
type = "rocksdb";
extraOpts = ''
open_files=2000
filter_bits=12
cache_mb=256
file_size_pb=8
file_size_mult=2;
'';
};
};
tempDb = mkOption {
description = "Rippled temporary database options.";
type = with types; nullOr (submodule dbOptions);
default = null;
};
importDb = mkOption {
description = "Settings for performing a one-time import.";
type = with types; nullOr (submodule dbOptions);
default = null;
};
nodeSize = mkOption {
description = ''
Rippled size of the node you are running.
"tiny", "small", "medium", "large", and "huge"
'';
type = types.enum ["tiny" "small" "medium" "large" "huge"];
default = "small";
};
ips = mkOption {
description = ''
List of hostnames or ips where the Ripple protocol is served.
For a starter list, you can either copy entries from:
https://ripple.com/ripple.txt or if you prefer you can let it
default to r.ripple.com 51235
A port may optionally be specified after adding a space to the
address. By convention, if known, IPs are listed in from most
to least trusted.
'';
type = types.listOf types.str;
default = ["r.ripple.com 51235"];
};
ipsFixed = mkOption {
description = ''
List of IP addresses or hostnames to which rippled should always
attempt to maintain peer connections with. This is useful for
manually forming private networks, for example to configure a
validation server that connects to the Ripple network through a
public-facing server, or for building a set of cluster peers.
A port may optionally be specified after adding a space to the address
'';
type = types.listOf types.str;
default = [];
};
validators = mkOption {
description = ''
List of nodes to always accept as validators. Nodes are specified by domain
or public key.
'';
type = types.listOf types.str;
default = [
"n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7 RL1"
"n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj RL2"
"n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C RL3"
"n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS RL4"
"n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA RL5"
];
};
databasePath = mkOption {
description = ''
Path to the ripple database.
'';
type = types.path;
default = "/var/lib/rippled";
};
validationQuorum = mkOption {
description = ''
The minimum number of trusted validations a ledger must have before
the server considers it fully validated.
'';
type = types.int;
default = 3;
};
ledgerHistory = mkOption {
description = ''
The number of past ledgers to acquire on server startup and the minimum
to maintain while running.
'';
type = types.either types.int (types.enum ["full"]);
default = 1296000; # 1 month
};
fetchDepth = mkOption {
description = ''
The number of past ledgers to serve to other peers that request historical
ledger data (or "full" for no limit).
'';
type = types.either types.int (types.enum ["full"]);
default = "full";
};
sntpServers = mkOption {
description = ''
IP address or domain of NTP servers to use for time synchronization.;
'';
type = types.listOf types.str;
default = [
"time.windows.com"
"time.apple.com"
"time.nist.gov"
"pool.ntp.org"
];
};
logLevel = mkOption {
description = "Logging verbosity.";
type = types.enum ["debug" "error" "info"];
default = "error";
};
statsd = {
enable = mkEnableOption "statsd monitoring for rippled";
address = mkOption {
description = "The UDP address and port of the listening StatsD server.";
default = "127.0.0.1:8125";
type = types.str;
};
prefix = mkOption {
description = "A string prepended to each collected metric.";
default = "";
type = types.str;
};
};
extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra lines to be added verbatim to the rippled.cfg configuration file.
'';
};
config = mkOption {
internal = true;
default = pkgs.writeText "rippled.conf" rippledCfg;
defaultText = literalDocBook "generated config file";
};
};
};
###### implementation
config = mkIf cfg.enable {
users.users.rippled = {
description = "Ripple server user";
isSystemUser = true;
group = "rippled";
home = cfg.databasePath;
createHome = true;
};
users.groups.rippled = {};
systemd.services.rippled = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/rippled --fg --conf ${cfg.config}";
User = "rippled";
Restart = "on-failure";
LimitNOFILE=10000;
};
};
environment.systemPackages = [ cfg.package ];
};
}

View file

@ -0,0 +1,147 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.rmfakecloud;
serviceDataDir = "/var/lib/rmfakecloud";
in {
options = {
services.rmfakecloud = {
enable = mkEnableOption "rmfakecloud remarkable self-hosted cloud";
package = mkOption {
type = types.package;
default = pkgs.rmfakecloud;
defaultText = literalExpression "pkgs.rmfakecloud";
description = ''
rmfakecloud package to use.
The default does not include the web user interface.
'';
};
storageUrl = mkOption {
type = types.str;
example = "https://local.appspot.com";
description = ''
URL used by the tablet to access the rmfakecloud service.
'';
};
port = mkOption {
type = types.port;
default = 3000;
description = ''
Listening port number.
'';
};
logLevel = mkOption {
type = types.enum [ "info" "debug" "warn" "error" ];
default = "info";
description = ''
Logging level.
'';
};
extraSettings = mkOption {
type = with types; attrsOf str;
default = { };
example = { DATADIR = "/custom/path/for/rmfakecloud/data"; };
description = ''
Extra settings in the form of a set of key-value pairs.
For tokens and secrets, use `environmentFile` instead.
Available settings are listed on
https://ddvk.github.io/rmfakecloud/install/configuration/.
'';
};
environmentFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/etc/secrets/rmfakecloud.env";
description = ''
Path to an environment file loaded for the rmfakecloud service.
This can be used to securely store tokens and secrets outside of the
world-readable Nix store. Since this file is read by systemd, it may
have permission 0400 and be owned by root.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.rmfakecloud = {
description = "rmfakecloud remarkable self-hosted cloud";
environment = {
STORAGE_URL = cfg.storageUrl;
PORT = toString cfg.port;
LOGLEVEL = cfg.logLevel;
} // cfg.extraSettings;
preStart = ''
# Generate the secret key used to sign client session tokens.
# Replacing it invalidates the previously established sessions.
if [ -z "$JWT_SECRET_KEY" ] && [ ! -f jwt_secret_key ]; then
(umask 077; touch jwt_secret_key)
cat /dev/urandom | tr -cd '[:alnum:]' | head -c 48 >> jwt_secret_key
fi
'';
script = ''
if [ -z "$JWT_SECRET_KEY" ]; then
export JWT_SECRET_KEY="$(cat jwt_secret_key)"
fi
${cfg.package}/bin/rmfakecloud
'';
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
Restart = "always";
EnvironmentFile =
mkIf (cfg.environmentFile != null) cfg.environmentFile;
AmbientCapabilities =
mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
DynamicUser = true;
PrivateDevices = true;
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
WorkingDirectory = serviceDataDir;
StateDirectory = baseNameOf serviceDataDir;
UMask = 0027;
};
};
};
meta.maintainers = with maintainers; [ pacien ];
}

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