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,53 @@
{ config, pkgs, lib, ... }:
let
cfg = config.services.meshcentral;
configFormat = pkgs.formats.json {};
configFile = configFormat.generate "meshcentral-config.json" cfg.settings;
in with lib; {
options.services.meshcentral = with types; {
enable = mkEnableOption "MeshCentral computer management server";
package = mkOption {
description = "MeshCentral package to use. Replacing this may be necessary to add dependencies for extra functionality.";
type = types.package;
default = pkgs.meshcentral;
defaultText = literalExpression "pkgs.meshcentral";
};
settings = mkOption {
description = ''
Settings for MeshCentral. Refer to upstream documentation for details:
<itemizedlist>
<listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json">JSON Schema definition</link></para></listitem>
<listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/sample-config.json">simple sample configuration</link></para></listitem>
<listitem><para><link xlink:href="https://github.com/Ylianst/MeshCentral/blob/master/sample-config-advanced.json">complex sample configuration</link></para></listitem>
<listitem><para><link xlink:href="https://www.meshcommander.com/meshcentral2">Old homepage) with documentation link</link></para></listitem>
</itemizedlist>
'';
type = types.submodule {
freeformType = configFormat.type;
};
example = {
settings = {
WANonly = true;
Cert = "meshcentral.example.com";
TlsOffload = "10.0.0.2,fd42::2";
Port = 4430;
};
domains."".certUrl = "https://meshcentral.example.com/";
};
};
};
config = mkIf cfg.enable {
services.meshcentral.settings.settings.autoBackup.backupPath = lib.mkDefault "/var/lib/meshcentral/backups";
systemd.services.meshcentral = {
wantedBy = ["multi-user.target"];
serviceConfig = {
ExecStart = "${cfg.package}/bin/meshcentral --datapath /var/lib/meshcentral --configfile ${configFile}";
DynamicUser = true;
StateDirectory = "meshcentral";
CacheDirectory = "meshcentral";
};
};
};
meta.maintainers = [ maintainers.lheckemann ];
}

View file

@ -0,0 +1,118 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.oxidized;
in
{
options.services.oxidized = {
enable = mkEnableOption "the oxidized configuration backup service";
user = mkOption {
type = types.str;
default = "oxidized";
description = ''
User under which the oxidized service runs.
'';
};
group = mkOption {
type = types.str;
default = "oxidized";
description = ''
Group under which the oxidized service runs.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/oxidized";
description = "State directory for the oxidized service.";
};
configFile = mkOption {
type = types.path;
example = literalExpression ''
pkgs.writeText "oxidized-config.yml" '''
---
debug: true
use_syslog: true
input:
default: ssh
ssh:
secure: true
interval: 3600
model_map:
dell: powerconnect
hp: procurve
source:
default: csv
csv:
delimiter: !ruby/regexp /:/
file: "/var/lib/oxidized/.config/oxidized/router.db"
map:
name: 0
model: 1
username: 2
password: 3
pid: "/var/lib/oxidized/.config/oxidized/pid"
rest: 127.0.0.1:8888
retries: 3
# ... additional config
''';
'';
description = ''
Path to the oxidized configuration file.
'';
};
routerDB = mkOption {
type = types.path;
example = literalExpression ''
pkgs.writeText "oxidized-router.db" '''
hostname-sw1:powerconnect:username1:password2
hostname-sw2:procurve:username2:password2
# ... additional hosts
'''
'';
description = ''
Path to the file/database which contains the targets for oxidized.
'';
};
};
config = mkIf cfg.enable {
users.groups.${cfg.group} = { };
users.users.${cfg.user} = {
description = "Oxidized service user";
group = cfg.group;
home = cfg.dataDir;
createHome = true;
isSystemUser = true;
};
systemd.services.oxidized = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
mkdir -p ${cfg.dataDir}/.config/oxidized
ln -f -s ${cfg.routerDB} ${cfg.dataDir}/.config/oxidized/router.db
ln -f -s ${cfg.configFile} ${cfg.dataDir}/.config/oxidized/config
'';
serviceConfig = {
ExecStart = "${pkgs.oxidized}/bin/oxidized";
User = cfg.user;
Group = cfg.group;
UMask = "0077";
NoNewPrivileges = true;
Restart = "always";
WorkingDirectory = cfg.dataDir;
KillSignal = "SIGKILL";
PIDFile = "${cfg.dataDir}/.config/oxidized/pid";
};
};
};
}

View file

@ -0,0 +1,127 @@
{ config, lib, pkgs, ... }:
with lib;
let
pkg = pkgs.pgadmin4;
cfg = config.services.pgadmin;
_base = with types; [ int bool str ];
base = with types; oneOf ([ (listOf (oneOf _base)) (attrsOf (oneOf _base)) ] ++ _base);
formatAttrset = attr:
"{${concatStringsSep "\n" (mapAttrsToList (key: value: "${builtins.toJSON key}: ${formatPyValue value},") attr)}}";
formatPyValue = value:
if builtins.isString value then builtins.toJSON value
else if value ? _expr then value._expr
else if builtins.isInt value then toString value
else if builtins.isBool value then (if value then "True" else "False")
else if builtins.isAttrs value then (formatAttrset value)
else if builtins.isList value then "[${concatStringsSep "\n" (map (v: "${formatPyValue v},") value)}]"
else throw "Unrecognized type";
formatPy = attrs:
concatStringsSep "\n" (mapAttrsToList (key: value: "${key} = ${formatPyValue value}") attrs);
pyType = with types; attrsOf (oneOf [ (attrsOf base) (listOf base) base ]);
in
{
options.services.pgadmin = {
enable = mkEnableOption "PostgreSQL Admin 4";
port = mkOption {
description = "Port for pgadmin4 to run on";
type = types.port;
default = 5050;
};
initialEmail = mkOption {
description = "Initial email for the pgAdmin account.";
type = types.str;
};
initialPasswordFile = mkOption {
description = ''
Initial password file for the pgAdmin account.
NOTE: Should be string not a store path, to prevent the password from being world readable.
'';
type = types.path;
};
openFirewall = mkEnableOption "firewall passthrough for pgadmin4";
settings = mkOption {
description = ''
Settings for pgadmin4.
<link xlink:href="https://www.pgadmin.org/docs/pgadmin4/development/config_py.html">Documentation</link>.
'';
type = pyType;
default= {};
};
};
config = mkIf (cfg.enable) {
networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
services.pgadmin.settings = {
DEFAULT_SERVER_PORT = cfg.port;
SERVER_MODE = true;
} // (optionalAttrs cfg.openFirewall {
DEFAULT_SERVER = mkDefault "::";
});
systemd.services.pgadmin = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
requires = [ "network.target" ];
# we're adding this optionally so just in case there's any race it'll be caught
# in case postgres doesn't start, pgadmin will just start normally
wants = [ "postgresql.service" ];
path = [ config.services.postgresql.package pkgs.coreutils pkgs.bash ];
preStart = ''
# NOTE: this is idempotent (aka running it twice has no effect)
(
# Email address:
echo ${escapeShellArg cfg.initialEmail}
# file might not contain newline. echo hack fixes that.
PW=$(cat ${escapeShellArg cfg.initialPasswordFile})
# Password:
echo "$PW"
# Retype password:
echo "$PW"
) | ${pkg}/bin/pgadmin4-setup
'';
restartTriggers = [
"/etc/pgadmin/config_system.py"
];
serviceConfig = {
User = "pgadmin";
DynamicUser = true;
LogsDirectory = "pgadmin";
StateDirectory = "pgadmin";
ExecStart = "${pkg}/bin/pgadmin4";
};
};
users.users.pgadmin = {
isSystemUser = true;
group = "pgadmin";
};
users.groups.pgadmin = {};
environment.etc."pgadmin/config_system.py" = {
text = formatPy cfg.settings;
mode = "0600";
user = "pgadmin";
group = "pgadmin";
};
};
}

View file

@ -0,0 +1,63 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.salt.master;
fullConfig = lib.recursiveUpdate {
# Provide defaults for some directories to allow an immutable config dir
# Default is equivalent to /etc/salt/master.d/*.conf
default_include = "/var/lib/salt/master.d/*.conf";
# Default is in /etc/salt/pki/master
pki_dir = "/var/lib/salt/pki/master";
} cfg.configuration;
in
{
options = {
services.salt.master = {
enable = mkEnableOption "Salt master service";
configuration = mkOption {
type = types.attrs;
default = {};
description = "Salt master configuration as Nix attribute set.";
};
};
};
config = mkIf cfg.enable {
environment = {
# Set this up in /etc/salt/master so `salt`, `salt-key`, etc. work.
# The alternatives are
# - passing --config-dir to all salt commands, not just the master unit,
# - setting a global environment variable,
etc."salt/master".source = pkgs.writeText "master" (
builtins.toJSON fullConfig
);
systemPackages = with pkgs; [ salt ];
};
systemd.services.salt-master = {
description = "Salt Master";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = with pkgs; [
util-linux # for dmesg
];
serviceConfig = {
ExecStart = "${pkgs.salt}/bin/salt-master";
LimitNOFILE = 16384;
Type = "notify";
NotifyAccess = "all";
};
restartTriggers = [
config.environment.etc."salt/master".source
];
};
};
meta.maintainers = with lib.maintainers; [ Flakebi ];
}

View file

@ -0,0 +1,67 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.salt.minion;
fullConfig = lib.recursiveUpdate {
# Provide defaults for some directories to allow an immutable config dir
# NOTE: the config dir being immutable prevents `minion_id` caching
# Default is equivalent to /etc/salt/minion.d/*.conf
default_include = "/var/lib/salt/minion.d/*.conf";
# Default is in /etc/salt/pki/minion
pki_dir = "/var/lib/salt/pki/minion";
} cfg.configuration;
in
{
options = {
services.salt.minion = {
enable = mkEnableOption "Salt minion service";
configuration = mkOption {
type = types.attrs;
default = {};
description = ''
Salt minion configuration as Nix attribute set.
See <link xlink:href="https://docs.saltstack.com/en/latest/ref/configuration/minion.html"/>
for details.
'';
};
};
};
config = mkIf cfg.enable {
environment = {
# Set this up in /etc/salt/minion so `salt-call`, etc. work.
# The alternatives are
# - passing --config-dir to all salt commands, not just the minion unit,
# - setting aglobal environment variable.
etc."salt/minion".source = pkgs.writeText "minion" (
builtins.toJSON fullConfig
);
systemPackages = with pkgs; [ salt ];
};
systemd.services.salt-minion = {
description = "Salt Minion";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = with pkgs; [
util-linux
];
serviceConfig = {
ExecStart = "${pkgs.salt}/bin/salt-minion";
LimitNOFILE = 8192;
Type = "notify";
NotifyAccess = "all";
};
restartTriggers = [
config.environment.etc."salt/minion".source
];
};
};
}

View file

@ -0,0 +1,19 @@
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.BrokerFactory;
import java.net.URI;
public class ActiveMQBroker {
public static void main(String[] args) throws Throwable {
URI uri = new URI((args.length > 0) ? args[0] : "xbean:activemq.xml");
BrokerService broker = BrokerFactory.createBroker(uri);
broker.start();
if (broker.waitUntilStarted()) {
broker.waitUntilStopped();
} else {
System.out.println("Failed starting broker");
System.exit(-1);
};
}
}

View file

@ -0,0 +1,135 @@
{ config, lib, pkgs, ... }:
with pkgs;
with lib;
let
cfg = config.services.activemq;
activemqBroker = stdenv.mkDerivation {
name = "activemq-broker";
phases = [ "installPhase" ];
buildInputs = [ jdk ];
installPhase = ''
mkdir -p $out/lib
source ${activemq}/lib/classpath.env
export CLASSPATH
ln -s "${./ActiveMQBroker.java}" ActiveMQBroker.java
javac -d $out/lib ActiveMQBroker.java
'';
};
in {
options = {
services.activemq = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable the Apache ActiveMQ message broker service.
'';
};
configurationDir = mkOption {
default = "${activemq}/conf";
defaultText = literalExpression ''"''${pkgs.activemq}/conf"'';
type = types.str;
description = ''
The base directory for ActiveMQ's configuration.
By default, this directory is searched for a file named activemq.xml,
which should contain the configuration for the broker service.
'';
};
configurationURI = mkOption {
type = types.str;
default = "xbean:activemq.xml";
description = ''
The URI that is passed along to the BrokerFactory to
set up the configuration of the ActiveMQ broker service.
You should not need to change this. For custom configuration,
set the <literal>configurationDir</literal> instead, and create
an activemq.xml configuration file in it.
'';
};
baseDir = mkOption {
type = types.str;
default = "/var/activemq";
description = ''
The base directory where ActiveMQ stores its persistent data and logs.
This will be overridden if you set "activemq.base" and "activemq.data"
in the <literal>javaProperties</literal> option. You can also override
this in activemq.xml.
'';
};
javaProperties = mkOption {
type = types.attrs;
default = { };
example = literalExpression ''
{
"java.net.preferIPv4Stack" = "true";
}
'';
apply = attrs: {
"activemq.base" = "${cfg.baseDir}";
"activemq.data" = "${cfg.baseDir}/data";
"activemq.conf" = "${cfg.configurationDir}";
"activemq.home" = "${activemq}";
} // attrs;
description = ''
Specifies Java properties that are sent to the ActiveMQ
broker service with the "-D" option. You can set properties
here to change the behaviour and configuration of the broker.
All essential properties that are not set here are automatically
given reasonable defaults.
'';
};
extraJavaOptions = mkOption {
type = types.separatedString " ";
default = "";
example = "-Xmx2G -Xms2G -XX:MaxPermSize=512M";
description = ''
Add extra options here that you want to be sent to the
Java runtime when the broker service is started.
'';
};
};
};
config = mkIf cfg.enable {
users.users.activemq = {
description = "ActiveMQ server user";
group = "activemq";
uid = config.ids.uids.activemq;
};
users.groups.activemq.gid = config.ids.gids.activemq;
systemd.services.activemq_init = {
wantedBy = [ "activemq.service" ];
partOf = [ "activemq.service" ];
before = [ "activemq.service" ];
serviceConfig.Type = "oneshot";
script = ''
mkdir -p "${cfg.javaProperties."activemq.data"}"
chown -R activemq "${cfg.javaProperties."activemq.data"}"
'';
};
systemd.services.activemq = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [ jre ];
serviceConfig.User = "activemq";
script = ''
source ${activemq}/lib/classpath.env
export CLASSPATH=${activemqBroker}/lib:${cfg.configurationDir}:$CLASSPATH
exec java \
${concatStringsSep " \\\n" (mapAttrsToList (name: value: "-D${name}=${value}") cfg.javaProperties)} \
${cfg.extraJavaOptions} ActiveMQBroker "${cfg.configurationURI}"
'';
};
};
}

View file

@ -0,0 +1,228 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.rabbitmq;
inherit (builtins) concatStringsSep;
config_file_content = lib.generators.toKeyValue { } cfg.configItems;
config_file = pkgs.writeText "rabbitmq.conf" config_file_content;
advanced_config_file = pkgs.writeText "advanced.config" cfg.config;
in
{
###### interface
options = {
services.rabbitmq = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the RabbitMQ server, an Advanced Message
Queuing Protocol (AMQP) broker.
'';
};
package = mkOption {
default = pkgs.rabbitmq-server;
type = types.package;
defaultText = literalExpression "pkgs.rabbitmq-server";
description = ''
Which rabbitmq package to use.
'';
};
listenAddress = mkOption {
default = "127.0.0.1";
example = "";
description = ''
IP address on which RabbitMQ will listen for AMQP
connections. Set to the empty string to listen on all
interfaces. Note that RabbitMQ creates a user named
<literal>guest</literal> with password
<literal>guest</literal> by default, so you should delete
this user if you intend to allow external access.
Together with 'port' setting it's mostly an alias for
configItems."listeners.tcp.1" and it's left for backwards
compatibility with previous version of this module.
'';
type = types.str;
};
port = mkOption {
default = 5672;
description = ''
Port on which RabbitMQ will listen for AMQP connections.
'';
type = types.port;
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/rabbitmq";
description = ''
Data directory for rabbitmq.
'';
};
cookie = mkOption {
default = "";
type = types.str;
description = ''
Erlang cookie is a string of arbitrary length which must
be the same for several nodes to be allowed to communicate.
Leave empty to generate automatically.
'';
};
configItems = mkOption {
default = { };
type = types.attrsOf types.str;
example = literalExpression ''
{
"auth_backends.1.authn" = "rabbit_auth_backend_ldap";
"auth_backends.1.authz" = "rabbit_auth_backend_internal";
}
'';
description = ''
Configuration options in RabbitMQ's new config file format,
which is a simple key-value format that can not express nested
data structures. This is known as the <literal>rabbitmq.conf</literal> file,
although outside NixOS that filename may have Erlang syntax, particularly
prior to RabbitMQ 3.7.0.
If you do need to express nested data structures, you can use
<literal>config</literal> option. Configuration from <literal>config</literal>
will be merged into these options by RabbitMQ at runtime to
form the final configuration.
See https://www.rabbitmq.com/configure.html#config-items
For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
'';
};
config = mkOption {
default = "";
type = types.str;
description = ''
Verbatim advanced configuration file contents using the Erlang syntax.
This is also known as the <literal>advanced.config</literal> file or the old config format.
<literal>configItems</literal> is preferred whenever possible. However, nested
data structures can only be expressed properly using the <literal>config</literal> option.
The contents of this option will be merged into the <literal>configItems</literal>
by RabbitMQ at runtime to form the final configuration.
See the second table on https://www.rabbitmq.com/configure.html#config-items
For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
'';
};
plugins = mkOption {
default = [ ];
type = types.listOf types.str;
description = "The names of plugins to enable";
};
pluginDirs = mkOption {
default = [ ];
type = types.listOf types.path;
description = "The list of directories containing external plugins";
};
managementPlugin = {
enable = mkEnableOption "the management plugin";
port = mkOption {
default = 15672;
type = types.port;
description = ''
On which port to run the management plugin
'';
};
};
};
};
###### implementation
config = mkIf cfg.enable {
# This is needed so we will have 'rabbitmqctl' in our PATH
environment.systemPackages = [ cfg.package ];
services.epmd.enable = true;
users.users.rabbitmq = {
description = "RabbitMQ server user";
home = "${cfg.dataDir}";
createHome = true;
group = "rabbitmq";
uid = config.ids.uids.rabbitmq;
};
users.groups.rabbitmq.gid = config.ids.gids.rabbitmq;
services.rabbitmq.configItems = {
"listeners.tcp.1" = mkDefault "${cfg.listenAddress}:${toString cfg.port}";
} // optionalAttrs cfg.managementPlugin.enable {
"management.tcp.port" = toString cfg.managementPlugin.port;
"management.tcp.ip" = cfg.listenAddress;
};
services.rabbitmq.plugins = optional cfg.managementPlugin.enable "rabbitmq_management";
systemd.services.rabbitmq = {
description = "RabbitMQ Server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "epmd.socket" ];
wants = [ "network.target" "epmd.socket" ];
path = [
cfg.package
pkgs.coreutils # mkdir/chown/chmod for preStart
];
environment = {
RABBITMQ_MNESIA_BASE = "${cfg.dataDir}/mnesia";
RABBITMQ_LOGS = "-";
SYS_PREFIX = "";
RABBITMQ_CONFIG_FILE = config_file;
RABBITMQ_PLUGINS_DIR = concatStringsSep ":" cfg.pluginDirs;
RABBITMQ_ENABLED_PLUGINS_FILE = pkgs.writeText "enabled_plugins" ''
[ ${concatStringsSep "," cfg.plugins} ].
'';
} // optionalAttrs (cfg.config != "") { RABBITMQ_ADVANCED_CONFIG_FILE = advanced_config_file; };
serviceConfig = {
ExecStart = "${cfg.package}/sbin/rabbitmq-server";
ExecStop = "${cfg.package}/sbin/rabbitmqctl shutdown";
User = "rabbitmq";
Group = "rabbitmq";
LogsDirectory = "rabbitmq";
WorkingDirectory = cfg.dataDir;
Type = "notify";
NotifyAccess = "all";
UMask = "0027";
LimitNOFILE = "100000";
Restart = "on-failure";
RestartSec = "10";
TimeoutStartSec = "3600";
};
preStart = ''
${optionalString (cfg.cookie != "") ''
echo -n ${cfg.cookie} > ${cfg.dataDir}/.erlang.cookie
chmod 600 ${cfg.dataDir}/.erlang.cookie
''}
'';
};
};
}

View file

@ -0,0 +1,133 @@
# ALSA sound support.
{ config, lib, pkgs, ... }:
with lib;
let
inherit (pkgs) alsa-utils;
pulseaudioEnabled = config.hardware.pulseaudio.enable;
in
{
imports = [
(mkRenamedOptionModule [ "sound" "enableMediaKeys" ] [ "sound" "mediaKeys" "enable" ])
];
###### interface
options = {
sound = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable ALSA sound.
'';
};
enableOSSEmulation = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable ALSA OSS emulation (with certain cards sound mixing may not work!).
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
defaults.pcm.!card 3
'';
description = ''
Set addition configuration for system-wide alsa.
'';
};
mediaKeys = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable volume and capture control with keyboard media keys.
You want to leave this disabled if you run a desktop environment
like KDE, Gnome, Xfce, etc, as those handle such things themselves.
You might want to enable this if you run a minimalistic desktop
environment or work from bare linux ttys/framebuffers.
Enabling this will turn on <option>services.actkbd</option>.
'';
};
volumeStep = mkOption {
type = types.str;
default = "1";
example = "1%";
description = ''
The value by which to increment/decrement volume on media keys.
See amixer(1) for allowed values.
'';
};
};
};
};
###### implementation
config = mkIf config.sound.enable {
environment.systemPackages = [ alsa-utils ];
environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "")
{ "asound.conf".text = config.sound.extraConfig; };
# ALSA provides a udev rule for restoring volume settings.
services.udev.packages = [ alsa-utils ];
boot.kernelModules = optional config.sound.enableOSSEmulation "snd_pcm_oss";
systemd.services.alsa-store =
{ description = "Store Sound Card State";
wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = "/var/lib/alsa";
unitConfig.ConditionVirtualization = "!systemd-nspawn";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/mkdir -p /var/lib/alsa";
ExecStop = "${alsa-utils}/sbin/alsactl store --ignore";
};
};
services.actkbd = mkIf config.sound.mediaKeys.enable {
enable = true;
bindings = [
# "Mute" media key
{ keys = [ 113 ]; events = [ "key" ]; command = "${alsa-utils}/bin/amixer -q set Master toggle"; }
# "Lower Volume" media key
{ keys = [ 114 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}- unmute"; }
# "Raise Volume" media key
{ keys = [ 115 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}+ unmute"; }
# "Mic Mute" media key
{ keys = [ 190 ]; events = [ "key" ]; command = "${alsa-utils}/bin/amixer -q set Capture toggle"; }
];
};
};
}

View file

@ -0,0 +1,115 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.botamusique;
format = pkgs.formats.ini {};
configFile = format.generate "botamusique.ini" cfg.settings;
in
{
meta.maintainers = with lib.maintainers; [ hexa ];
options.services.botamusique = {
enable = mkEnableOption "botamusique, a bot to play audio streams on mumble";
package = mkOption {
type = types.package;
default = pkgs.botamusique;
defaultText = literalExpression "pkgs.botamusique";
description = "The botamusique package to use.";
};
settings = mkOption {
type = with types; submodule {
freeformType = format.type;
options = {
server.host = mkOption {
type = types.str;
default = "localhost";
example = "mumble.example.com";
description = "Hostname of the mumble server to connect to.";
};
server.port = mkOption {
type = types.port;
default = 64738;
description = "Port of the mumble server to connect to.";
};
bot.username = mkOption {
type = types.str;
default = "botamusique";
description = "Name the bot should appear with.";
};
bot.comment = mkOption {
type = types.str;
default = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!";
description = "Comment displayed for the bot.";
};
};
};
default = {};
description = ''
Your <filename>configuration.ini</filename> as a Nix attribute set. Look up
possible options in the <link xlink:href="https://github.com/azlux/botamusique/blob/master/configuration.example.ini">configuration.example.ini</link>.
'';
};
};
config = mkIf cfg.enable {
systemd.services.botamusique = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.Documentation = "https://github.com/azlux/botamusique/wiki";
environment.HOME = "/var/lib/botamusique";
serviceConfig = {
ExecStart = "${cfg.package}/bin/botamusique --config ${configFile}";
Restart = "always"; # the bot exits when the server connection is lost
# Hardening
CapabilityBoundingSet = [ "" ];
DynamicUser = true;
IPAddressDeny = [
"link-local"
"multicast"
];
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProcSubset = "pid";
PrivateDevices = true;
PrivateUsers = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
StateDirectory = "botamusique";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
WorkingDirectory = "/var/lib/botamusique";
};
};
};
}

View file

@ -0,0 +1,142 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.hqplayerd;
pkg = pkgs.hqplayerd;
# XXX: This is hard-coded in the distributed binary, don't try to change it.
stateDir = "/var/lib/hqplayer";
configDir = "/etc/hqplayer";
in
{
options = {
services.hqplayerd = {
enable = mkEnableOption "HQPlayer Embedded";
auth = {
username = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Username used for HQPlayer's WebUI.
Without this you will need to manually create the credentials after
first start by going to http://your.ip/8088/auth
'';
};
password = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Password used for HQPlayer's WebUI.
Without this you will need to manually create the credentials after
first start by going to http://your.ip/8088/auth
'';
};
};
licenseFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the HQPlayer license key file.
Without this, the service will run in trial mode and restart every 30
minutes.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Opens ports needed for the WebUI and controller API.
'';
};
config = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
HQplayer daemon configuration, written to /etc/hqplayer/hqplayerd.xml.
Refer to share/doc/hqplayerd/readme.txt in the hqplayerd derivation for possible values.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (cfg.auth.username != null -> cfg.auth.password != null)
&& (cfg.auth.password != null -> cfg.auth.username != null);
message = "You must set either both services.hqplayer.auth.username and password, or neither.";
}
];
environment = {
etc = {
"hqplayer/hqplayerd.xml" = mkIf (cfg.config != null) { source = pkgs.writeText "hqplayerd.xml" cfg.config; };
"hqplayer/hqplayerd4-key.xml" = mkIf (cfg.licenseFile != null) { source = cfg.licenseFile; };
"modules-load.d/taudio2.conf".source = "${pkg}/etc/modules-load.d/taudio2.conf";
};
systemPackages = [ pkg ];
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 8088 4321 ];
};
services.udev.packages = [ pkg ];
systemd = {
tmpfiles.rules = [
"d ${configDir} 0755 hqplayer hqplayer - -"
"d ${stateDir} 0755 hqplayer hqplayer - -"
"d ${stateDir}/home 0755 hqplayer hqplayer - -"
];
packages = [ pkg ];
services.hqplayerd = {
wantedBy = [ "multi-user.target" ];
after = [ "systemd-tmpfiles-setup.service" ];
environment.HOME = "${stateDir}/home";
unitConfig.ConditionPathExists = [ configDir stateDir ];
restartTriggers = optionals (cfg.config != null) [ config.environment.etc."hqplayer/hqplayerd.xml".source ];
preStart = ''
cp -r "${pkg}/var/lib/hqplayer/web" "${stateDir}"
chmod -R u+wX "${stateDir}/web"
if [ ! -f "${configDir}/hqplayerd.xml" ]; then
echo "creating initial config file"
install -m 0644 "${pkg}/etc/hqplayer/hqplayerd.xml" "${configDir}/hqplayerd.xml"
fi
'' + optionalString (cfg.auth.username != null && cfg.auth.password != null) ''
${pkg}/bin/hqplayerd -s ${cfg.auth.username} ${cfg.auth.password}
'';
};
};
users.groups = {
hqplayer.gid = config.ids.gids.hqplayer;
};
users.users = {
hqplayer = {
description = "hqplayer daemon user";
extraGroups = [ "audio" ];
group = "hqplayer";
uid = config.ids.uids.hqplayer;
};
};
};
}

View file

@ -0,0 +1,131 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.icecast;
configFile = pkgs.writeText "icecast.xml" ''
<icecast>
<hostname>${cfg.hostname}</hostname>
<authentication>
<admin-user>${cfg.admin.user}</admin-user>
<admin-password>${cfg.admin.password}</admin-password>
</authentication>
<paths>
<logdir>${cfg.logDir}</logdir>
<adminroot>${pkgs.icecast}/share/icecast/admin</adminroot>
<webroot>${pkgs.icecast}/share/icecast/web</webroot>
<alias source="/" dest="/status.xsl"/>
</paths>
<listen-socket>
<port>${toString cfg.listen.port}</port>
<bind-address>${cfg.listen.address}</bind-address>
</listen-socket>
<security>
<chroot>0</chroot>
<changeowner>
<user>${cfg.user}</user>
<group>${cfg.group}</group>
</changeowner>
</security>
${cfg.extraConf}
</icecast>
'';
in {
###### interface
options = {
services.icecast = {
enable = mkEnableOption "Icecast server";
hostname = mkOption {
type = types.nullOr types.str;
description = "DNS name or IP address that will be used for the stream directory lookups or possibily the playlist generation if a Host header is not provided.";
default = config.networking.domain;
defaultText = literalExpression "config.networking.domain";
};
admin = {
user = mkOption {
type = types.str;
description = "Username used for all administration functions.";
default = "admin";
};
password = mkOption {
type = types.str;
description = "Password used for all administration functions.";
};
};
logDir = mkOption {
type = types.path;
description = "Base directory used for logging.";
default = "/var/log/icecast";
};
listen = {
port = mkOption {
type = types.int;
description = "TCP port that will be used to accept client connections.";
default = 8000;
};
address = mkOption {
type = types.str;
description = "Address Icecast will listen on.";
default = "::";
};
};
user = mkOption {
type = types.str;
description = "User privileges for the server.";
default = "nobody";
};
group = mkOption {
type = types.str;
description = "Group privileges for the server.";
default = "nogroup";
};
extraConf = mkOption {
type = types.lines;
description = "icecast.xml content.";
default = "";
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.icecast = {
after = [ "network.target" ];
description = "Icecast Network Audio Streaming Server";
wantedBy = [ "multi-user.target" ];
preStart = "mkdir -p ${cfg.logDir} && chown ${cfg.user}:${cfg.group} ${cfg.logDir}";
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.icecast}/bin/icecast -c ${configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View file

@ -0,0 +1,294 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.jack;
pcmPlugin = cfg.jackd.enable && cfg.alsa.enable;
loopback = cfg.jackd.enable && cfg.loopback.enable;
enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null;
umaskNeeded = versionOlder cfg.jackd.package.version "1.9.12";
bridgeNeeded = versionAtLeast cfg.jackd.package.version "1.9.12";
in {
options = {
services.jack = {
jackd = {
enable = mkEnableOption ''
JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group
'';
package = mkOption {
# until jack1 promiscuous mode is fixed
internal = true;
type = types.package;
default = pkgs.jack2;
defaultText = literalExpression "pkgs.jack2";
example = literalExpression "pkgs.jack1";
description = ''
The JACK package to use.
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [
"-dalsa"
];
example = literalExpression ''
[ "-dalsa" "--device" "hw:1" ];
'';
description = ''
Specifies startup command line arguments to pass to JACK server.
'';
};
session = mkOption {
type = types.lines;
description = ''
Commands to run after JACK is started.
'';
};
};
alsa = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin.
'';
};
support32Bit = mkOption {
type = types.bool;
default = false;
description = ''
Whether to support sound for 32-bit ALSA applications on 64-bit system.
'';
};
};
loopback = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Create ALSA loopback device, instead of using PCM plugin. Has broader
application support (things like Steam will work), but may need fine-tuning
for concrete hardware.
'';
};
index = mkOption {
type = types.int;
default = 10;
description = ''
Index of an ALSA loopback device.
'';
};
config = mkOption {
type = types.lines;
description = ''
ALSA config for loopback device.
'';
};
dmixConfig = mkOption {
type = types.lines;
default = "";
example = ''
period_size 2048
periods 2
'';
description = ''
For music production software that still doesn't support JACK natively you
would like to put buffer/period adjustments here
to decrease dmix device latency.
'';
};
session = mkOption {
type = types.lines;
description = ''
Additional commands to run to setup loopback device.
'';
};
};
};
};
config = mkMerge [
(mkIf pcmPlugin {
sound.extraConfig = ''
pcm_type.jack {
libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
${lib.optionalString enable32BitAlsaPlugins
"libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"}
}
pcm.!default {
@func getenv
vars [ PCM ]
default "plug:jack"
}
'';
})
(mkIf loopback {
boot.kernelModules = [ "snd-aloop" ];
boot.kernelParams = [ "snd-aloop.index=${toString cfg.loopback.index}" ];
sound.extraConfig = cfg.loopback.config;
})
(mkIf cfg.jackd.enable {
services.jack.jackd.session = ''
${lib.optionalString bridgeNeeded "${pkgs.a2jmidid}/bin/a2jmidid -e &"}
'';
# https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge#id06
services.jack.loopback.config = ''
pcm.loophw00 {
type hw
card ${toString cfg.loopback.index}
device 0
subdevice 0
}
pcm.amix {
type dmix
ipc_key 219345
slave {
pcm loophw00
${cfg.loopback.dmixConfig}
}
}
pcm.asoftvol {
type softvol
slave.pcm "amix"
control { name Master }
}
pcm.cloop {
type hw
card ${toString cfg.loopback.index}
device 1
subdevice 0
format S32_LE
}
pcm.loophw01 {
type hw
card ${toString cfg.loopback.index}
device 0
subdevice 1
}
pcm.ploop {
type hw
card ${toString cfg.loopback.index}
device 1
subdevice 1
format S32_LE
}
pcm.aduplex {
type asym
playback.pcm "asoftvol"
capture.pcm "loophw01"
}
pcm.!default {
type plug
slave.pcm aduplex
}
'';
services.jack.loopback.session = ''
alsa_in -j cloop -dcloop &
alsa_out -j ploop -dploop &
while [ "$(jack_lsp cloop)" == "" ] || [ "$(jack_lsp ploop)" == "" ]; do sleep 1; done
jack_connect cloop:capture_1 system:playback_1
jack_connect cloop:capture_2 system:playback_2
jack_connect system:capture_1 ploop:playback_1
jack_connect system:capture_2 ploop:playback_2
'';
assertions = [
{
assertion = !(cfg.alsa.enable && cfg.loopback.enable);
message = "For JACK both alsa and loopback options shouldn't be used at the same time.";
}
];
users.users.jackaudio = {
group = "jackaudio";
extraGroups = [ "audio" ];
description = "JACK Audio system service user";
isSystemUser = true;
};
# http://jackaudio.org/faq/linux_rt_config.html
security.pam.loginLimits = [
{ domain = "@jackaudio"; type = "-"; item = "rtprio"; value = "99"; }
{ domain = "@jackaudio"; type = "-"; item = "memlock"; value = "unlimited"; }
];
users.groups.jackaudio = {};
environment = {
systemPackages = [ cfg.jackd.package ];
etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf";
variables.JACK_PROMISCUOUS_SERVER = "jackaudio";
};
services.udev.extraRules = ''
ACTION=="add", SUBSYSTEM=="sound", ATTRS{id}!="Loopback", TAG+="systemd", ENV{SYSTEMD_WANTS}="jack.service"
'';
systemd.services.jack = {
description = "JACK Audio Connection Kit";
serviceConfig = {
User = "jackaudio";
SupplementaryGroups = lib.optional
(config.hardware.pulseaudio.enable
&& !config.hardware.pulseaudio.systemWide) "users";
ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
LimitRTPRIO = 99;
LimitMEMLOCK = "infinity";
} // optionalAttrs umaskNeeded {
UMask = "007";
};
path = [ cfg.jackd.package ];
environment = {
JACK_PROMISCUOUS_SERVER = "jackaudio";
JACK_NO_AUDIO_RESERVATION = "1";
};
restartIfChanged = false;
};
systemd.services.jack-session = {
description = "JACK session";
script = ''
jack_wait -w
${cfg.jackd.session}
${lib.optionalString cfg.loopback.enable cfg.loopback.session}
'';
serviceConfig = {
RemainAfterExit = true;
User = "jackaudio";
StateDirectory = "jack";
LimitRTPRIO = 99;
LimitMEMLOCK = "infinity";
};
path = [ cfg.jackd.package ];
environment = {
JACK_PROMISCUOUS_SERVER = "jackaudio";
HOME = "/var/lib/jack";
};
wantedBy = [ "jack.service" ];
partOf = [ "jack.service" ];
after = [ "jack.service" ];
restartIfChanged = false;
};
})
];
meta.maintainers = [ ];
}

View file

@ -0,0 +1,48 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.jmusicbot;
in
{
options = {
services.jmusicbot = {
enable = mkEnableOption "jmusicbot, a Discord music bot that's easy to set up and run yourself";
package = mkOption {
type = types.package;
default = pkgs.jmusicbot;
defaultText = literalExpression "pkgs.jmusicbot";
description = "JMusicBot package to use";
};
stateDir = mkOption {
type = types.path;
description = ''
The directory where config.txt and serversettings.json is saved.
If left as the default value this directory will automatically be created before JMusicBot starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
Untouched by the value of this option config.txt needs to be placed manually into this directory.
'';
default = "/var/lib/jmusicbot/";
};
};
};
config = mkIf cfg.enable {
systemd.services.jmusicbot = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
description = "Discord music bot that's easy to set up and run yourself!";
serviceConfig = mkMerge [{
ExecStart = "${cfg.package}/bin/JMusicBot";
WorkingDirectory = cfg.stateDir;
Restart = "always";
RestartSec = 20;
DynamicUser = true;
}
(mkIf (cfg.stateDir == "/var/lib/jmusicbot") { StateDirectory = "jmusicbot"; })];
};
};
meta.maintainers = with maintainers; [ SuperSandro2000 ];
}

View file

@ -0,0 +1,69 @@
{ config, lib, pkgs, ... }:
with lib;
let
streams = builtins.attrNames config.services.liquidsoap.streams;
streamService =
name:
let stream = builtins.getAttr name config.services.liquidsoap.streams; in
{ inherit name;
value = {
after = [ "network-online.target" "sound.target" ];
description = "${name} liquidsoap stream";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.wget ];
serviceConfig = {
ExecStart = "${pkgs.liquidsoap}/bin/liquidsoap ${stream}";
User = "liquidsoap";
LogsDirectory = "liquidsoap";
};
};
};
in
{
##### interface
options = {
services.liquidsoap.streams = mkOption {
description =
''
Set of Liquidsoap streams to start,
one systemd service per stream.
'';
default = {};
example = {
myStream1 = "/etc/liquidsoap/myStream1.liq";
myStream2 = literalExpression "./myStream2.liq";
myStream3 = "out(playlist(\"/srv/music/\"))";
};
type = types.attrsOf (types.either types.path types.str);
};
};
##### implementation
config = mkIf (builtins.length streams != 0) {
users.users.liquidsoap = {
uid = config.ids.uids.liquidsoap;
group = "liquidsoap";
extraGroups = [ "audio" ];
description = "Liquidsoap streaming user";
home = "/var/lib/liquidsoap";
createHome = true;
};
users.groups.liquidsoap.gid = config.ids.gids.liquidsoap;
systemd.services = builtins.listToAttrs ( map streamService streams );
};
}

View file

@ -0,0 +1,108 @@
{ config, lib, pkgs, ... }:
with pkgs;
with lib;
let
uid = config.ids.uids.mopidy;
gid = config.ids.gids.mopidy;
cfg = config.services.mopidy;
mopidyConf = writeText "mopidy.conf" cfg.configuration;
mopidyEnv = buildEnv {
name = "mopidy-with-extensions-${mopidy.version}";
paths = closePropagation cfg.extensionPackages;
pathsToLink = [ "/${mopidyPackages.python.sitePackages}" ];
buildInputs = [ makeWrapper ];
postBuild = ''
makeWrapper ${mopidy}/bin/mopidy $out/bin/mopidy \
--prefix PYTHONPATH : $out/${mopidyPackages.python.sitePackages}
'';
};
in {
options = {
services.mopidy = {
enable = mkEnableOption "Mopidy, a music player daemon";
dataDir = mkOption {
default = "/var/lib/mopidy";
type = types.str;
description = ''
The directory where Mopidy stores its state.
'';
};
extensionPackages = mkOption {
default = [];
type = types.listOf types.package;
example = literalExpression "[ pkgs.mopidy-spotify ]";
description = ''
Mopidy extensions that should be loaded by the service.
'';
};
configuration = mkOption {
default = "";
type = types.lines;
description = ''
The configuration that Mopidy should use.
'';
};
extraConfigFiles = mkOption {
default = [];
type = types.listOf types.str;
description = ''
Extra config file read by Mopidy when the service starts.
Later files in the list overrides earlier configuration.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - mopidy mopidy - -"
];
systemd.services.mopidy = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "sound.target" ];
description = "mopidy music player daemon";
serviceConfig = {
ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)}";
User = "mopidy";
};
};
systemd.services.mopidy-scan = {
description = "mopidy local files scanner";
serviceConfig = {
ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)} local scan";
User = "mopidy";
Type = "oneshot";
};
};
users.users.mopidy = {
inherit uid;
group = "mopidy";
extraGroups = [ "audio" ];
description = "Mopidy daemon user";
home = cfg.dataDir;
};
users.groups.mopidy.gid = gid;
};
}

View file

@ -0,0 +1,265 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "mpd";
uid = config.ids.uids.mpd;
gid = config.ids.gids.mpd;
cfg = config.services.mpd;
credentialsPlaceholder = (creds:
let
placeholders = (imap0
(i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
creds);
in
concatStringsSep "\n" placeholders);
mpdConf = pkgs.writeText "mpd.conf" ''
# This file was automatically generated by NixOS. Edit mpd's configuration
# via NixOS' configuration.nix, as this file will be rewritten upon mpd's
# restart.
music_directory "${cfg.musicDirectory}"
playlist_directory "${cfg.playlistDirectory}"
${lib.optionalString (cfg.dbFile != null) ''
db_file "${cfg.dbFile}"
''}
state_file "${cfg.dataDir}/state"
sticker_file "${cfg.dataDir}/sticker.sql"
${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''}
${optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''}
${optionalString (cfg.fluidsynth) ''
decoder {
plugin "fluidsynth"
soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
}
''}
${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
${cfg.extraConfig}
'';
in {
###### interface
options = {
services.mpd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable MPD, the music player daemon.
'';
};
startWhenNeeded = mkOption {
type = types.bool;
default = false;
description = ''
If set, <command>mpd</command> is socket-activated; that
is, instead of having it permanently running as a daemon,
systemd will start it on the first incoming connection.
'';
};
musicDirectory = mkOption {
type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
default = "${cfg.dataDir}/music";
defaultText = literalExpression ''"''${dataDir}/music"'';
description = ''
The directory or NFS/SMB network share where MPD reads music from. If left
as the default value this directory will automatically be created before
the MPD server starts, otherwise the sysadmin is responsible for ensuring
the directory exists with appropriate ownership and permissions.
'';
};
playlistDirectory = mkOption {
type = types.path;
default = "${cfg.dataDir}/playlists";
defaultText = literalExpression ''"''${dataDir}/playlists"'';
description = ''
The directory where MPD stores playlists. If left as the default value
this directory will automatically be created before the MPD server starts,
otherwise the sysadmin is responsible for ensuring the directory exists
with appropriate ownership and permissions.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra directives added to to the end of MPD's configuration file,
mpd.conf. Basic configuration like file location and uid/gid
is added automatically to the beginning of the file. For available
options see <literal>man 5 mpd.conf</literal>'.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/${name}";
description = ''
The directory where MPD stores its state, tag cache, playlists etc. If
left as the default value this directory will automatically be created
before the MPD server starts, otherwise the sysadmin is responsible for
ensuring the directory exists with appropriate ownership and permissions.
'';
};
user = mkOption {
type = types.str;
default = name;
description = "User account under which MPD runs.";
};
group = mkOption {
type = types.str;
default = name;
description = "Group account under which MPD runs.";
};
network = {
listenAddress = mkOption {
type = types.str;
default = "127.0.0.1";
example = "any";
description = ''
The address for the daemon to listen on.
Use <literal>any</literal> to listen on all addresses.
'';
};
port = mkOption {
type = types.int;
default = 6600;
description = ''
This setting is the TCP port that is desired for the daemon to get assigned
to.
'';
};
};
dbFile = mkOption {
type = types.nullOr types.str;
default = "${cfg.dataDir}/tag_cache";
defaultText = literalExpression ''"''${dataDir}/tag_cache"'';
description = ''
The path to MPD's database. If set to <literal>null</literal> the
parameter is omitted from the configuration.
'';
};
credentials = mkOption {
type = types.listOf (types.submodule {
options = {
passwordFile = mkOption {
type = types.path;
description = ''
Path to file containing the password.
'';
};
permissions = let
perms = ["read" "add" "control" "admin"];
in mkOption {
type = types.listOf (types.enum perms);
default = [ "read" ];
description = ''
List of permissions that are granted with this password.
Permissions can be "${concatStringsSep "\", \"" perms}".
'';
};
};
});
description = ''
Credentials and permissions for accessing the mpd server.
'';
default = [];
example = [
{passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];}
{passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];}
];
};
fluidsynth = mkOption {
type = types.bool;
default = false;
description = ''
If set, add fluidsynth soundfont and configure the plugin.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
# install mpd units
systemd.packages = [ pkgs.mpd ];
systemd.sockets.mpd = mkIf cfg.startWhenNeeded {
wantedBy = [ "sockets.target" ];
listenStreams = [
(if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
then cfg.network.listenAddress
else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
];
};
systemd.services.mpd = {
wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
preStart =
''
set -euo pipefail
install -m 600 ${mpdConf} /run/mpd/mpd.conf
'' + optionalString (cfg.credentials != [])
(concatStringsSep "\n"
(imap0
(i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'')
cfg.credentials));
serviceConfig =
{
User = "${cfg.user}";
# Note: the first "" overrides the ExecStart from the upstream unit
ExecStart = [ "" "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf" ];
RuntimeDirectory = "mpd";
StateDirectory = []
++ optionals (cfg.dataDir == "/var/lib/${name}") [ name ]
++ optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ]
++ optionals (cfg.musicDirectory == "/var/lib/${name}/music") [ name "${name}/music" ];
};
};
users.users = optionalAttrs (cfg.user == name) {
${name} = {
inherit uid;
group = cfg.group;
extraGroups = [ "audio" ];
description = "Music Player Daemon user";
home = "${cfg.dataDir}";
};
};
users.groups = optionalAttrs (cfg.group == name) {
${name}.gid = gid;
};
};
}

View file

@ -0,0 +1,213 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.mpdscribble;
mpdCfg = config.services.mpd;
mpdOpt = options.services.mpd;
endpointUrls = {
"last.fm" = "http://post.audioscrobbler.com";
"libre.fm" = "http://turtle.libre.fm";
"jamendo" = "http://postaudioscrobbler.jamendo.com";
"listenbrainz" = "http://proxy.listenbrainz.org";
};
mkSection = secname: secCfg: ''
[${secname}]
url = ${secCfg.url}
username = ${secCfg.username}
password = {{${secname}_PASSWORD}}
journal = /var/lib/mpdscribble/${secname}.journal
'';
endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints);
cfgTemplate = pkgs.writeText "mpdscribble.conf" ''
## This file was automatically genenrated by NixOS and will be overwritten.
## Do not edit. Edit your NixOS configuration instead.
## mpdscribble - an audioscrobbler for the Music Player Daemon.
## http://mpd.wikia.com/wiki/Client:mpdscribble
# HTTP proxy URL.
${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"}
# The location of the mpdscribble log file. The special value
# "syslog" makes mpdscribble use the local syslog daemon. On most
# systems, log messages will appear in /var/log/daemon.log then.
# "-" means log to stderr (the current terminal).
log = -
# How verbose mpdscribble's logging should be. Default is 1.
verbose = ${toString cfg.verbose}
# How often should mpdscribble save the journal file? [seconds]
journal_interval = ${toString cfg.journalInterval}
# The host running MPD, possibly protected by a password
# ([PASSWORD@]HOSTNAME).
host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host}
# The port that the MPD listens on and mpdscribble should try to
# connect to.
port = ${toString cfg.port}
${endpoints}
'';
cfgFile = "/run/mpdscribble/mpdscribble.conf";
replaceSecret = secretFile: placeholder: targetFile:
optionalString (secretFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '';
preStart = pkgs.writeShellScript "mpdscribble-pre-start" ''
cp -f "${cfgTemplate}" "${cfgFile}"
${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile}
${concatStringsSep "\n" (mapAttrsToList (secname: cfg:
replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile)
cfg.endpoints)}
'';
localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1");
in {
###### interface
options.services.mpdscribble = {
enable = mkEnableOption "mpdscribble";
proxy = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
HTTP proxy URL.
'';
};
verbose = mkOption {
default = 1;
type = types.int;
description = ''
Log level for the mpdscribble daemon.
'';
};
journalInterval = mkOption {
default = 600;
example = 60;
type = types.int;
description = ''
How often should mpdscribble save the journal file? [seconds]
'';
};
host = mkOption {
default = (if mpdCfg.network.listenAddress != "any" then
mpdCfg.network.listenAddress
else
"localhost");
defaultText = literalExpression ''
if config.${mpdOpt.network.listenAddress} != "any"
then config.${mpdOpt.network.listenAddress}
else "localhost"
'';
type = types.str;
description = ''
Host for the mpdscribble daemon to search for a mpd daemon on.
'';
};
passwordFile = mkOption {
default = if localMpd then
(findFirst
(c: any (x: x == "read") c.permissions)
{ passwordFile = null; }
mpdCfg.credentials).passwordFile
else
null;
defaultText = literalDocBook ''
The first password file with read access configured for MPD when using a local instance,
otherwise <literal>null</literal>.
'';
type = types.nullOr types.str;
description = ''
File containing the password for the mpd daemon.
If there is a local mpd configured using <option>services.mpd.credentials</option>
the default is automatically set to a matching passwordFile of the local mpd.
'';
};
port = mkOption {
default = mpdCfg.network.port;
defaultText = literalExpression "config.${mpdOpt.network.port}";
type = types.port;
description = ''
Port for the mpdscribble daemon to search for a mpd daemon on.
'';
};
endpoints = mkOption {
type = (let
endpoint = { name, ... }: {
options = {
url = mkOption {
type = types.str;
default = endpointUrls.${name} or "";
description =
"The url endpoint where the scrobble API is listening.";
};
username = mkOption {
type = types.str;
description = ''
Username for the scrobble service.
'';
};
passwordFile = mkOption {
type = types.nullOr types.str;
description =
"File containing the password, either as MD5SUM or cleartext.";
};
};
};
in types.attrsOf (types.submodule endpoint));
default = { };
example = {
"last.fm" = {
username = "foo";
passwordFile = "/run/secrets/lastfm_password";
};
};
description = ''
Endpoints to scrobble to.
If the endpoint is one of "${
concatStringsSep "\", \"" (attrNames endpointUrls)
}" the url is set automatically.
'';
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.mpdscribble = {
after = [ "network.target" ] ++ (optional localMpd "mpd.service");
description = "mpdscribble mpd scrobble client";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "mpdscribble";
RuntimeDirectory = "mpdscribble";
RuntimeDirectoryMode = "700";
# TODO use LoadCredential= instead of running preStart with full privileges?
ExecStartPre = "+${preStart}";
ExecStart =
"${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}";
};
};
};
}

View file

@ -0,0 +1,71 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.navidrome;
settingsFormat = pkgs.formats.json {};
in {
options = {
services.navidrome = {
enable = mkEnableOption "Navidrome music server";
settings = mkOption rec {
type = settingsFormat.type;
apply = recursiveUpdate default;
default = {
Address = "127.0.0.1";
Port = 4533;
};
example = {
MusicFolder = "/mnt/music";
};
description = ''
Configuration for Navidrome, see <link xlink:href="https://www.navidrome.org/docs/usage/configuration-options/"/> for supported values.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.navidrome = {
description = "Navidrome Media Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''
${pkgs.navidrome}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
'';
DynamicUser = true;
StateDirectory = "navidrome";
WorkingDirectory = "/var/lib/navidrome";
RuntimeDirectory = "navidrome";
RootDirectory = "/run/navidrome";
ReadWritePaths = "";
BindReadOnlyPaths = [
builtins.storeDir
] ++ lib.optional (cfg.settings ? MusicFolder) cfg.settings.MusicFolder;
CapabilityBoundingSet = "";
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
RestrictRealtime = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
UMask = "0066";
ProtectHostname = true;
};
};
};
}

View file

@ -0,0 +1,19 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "networkaudiod";
cfg = config.services.networkaudiod;
in {
options = {
services.networkaudiod = {
enable = mkEnableOption "Networkaudiod (NAA)";
};
};
config = mkIf cfg.enable {
systemd.packages = [ pkgs.networkaudiod ];
systemd.services.networkaudiod.wantedBy = [ "multi-user.target" ];
};
}

View file

@ -0,0 +1,76 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "roon-bridge";
cfg = config.services.roon-bridge;
in {
options = {
services.roon-bridge = {
enable = mkEnableOption "Roon Bridge";
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for the bridge.
'';
};
user = mkOption {
type = types.str;
default = "roon-bridge";
description = ''
User to run the Roon bridge as.
'';
};
group = mkOption {
type = types.str;
default = "roon-bridge";
description = ''
Group to run the Roon Bridge as.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.roon-bridge = {
after = [ "network.target" ];
description = "Roon Bridge";
wantedBy = [ "multi-user.target" ];
environment.ROON_DATAROOT = "/var/lib/${name}";
serviceConfig = {
ExecStart = "${pkgs.roon-bridge}/start.sh";
LimitNOFILE = 8192;
User = cfg.user;
Group = cfg.group;
StateDirectory = name;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
allowedUDPPorts = [ 9003 ];
extraCommands = ''
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
'';
};
users.groups.${cfg.group} = {};
users.users.${cfg.user} =
if cfg.user == "roon-bridge" then {
isSystemUser = true;
description = "Roon Bridge user";
group = cfg.group;
extraGroups = [ "audio" ];
}
else {};
};
}

View file

@ -0,0 +1,79 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "roon-server";
cfg = config.services.roon-server;
in {
options = {
services.roon-server = {
enable = mkEnableOption "Roon Server";
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for the server.
'';
};
user = mkOption {
type = types.str;
default = "roon-server";
description = ''
User to run the Roon Server as.
'';
};
group = mkOption {
type = types.str;
default = "roon-server";
description = ''
Group to run the Roon Server as.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.roon-server = {
after = [ "network.target" ];
description = "Roon Server";
wantedBy = [ "multi-user.target" ];
environment.ROON_DATAROOT = "/var/lib/${name}";
serviceConfig = {
ExecStart = "${pkgs.roon-server}/bin/RoonServer";
LimitNOFILE = 8192;
User = cfg.user;
Group = cfg.group;
StateDirectory = name;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPortRanges = [
{ from = 9100; to = 9200; }
{ from = 9330; to = 9332; }
];
allowedUDPPorts = [ 9003 ];
extraCommands = ''
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
'';
};
users.groups.${cfg.group} = {};
users.users.${cfg.user} =
if cfg.user == "roon-server" then {
isSystemUser = true;
description = "Roon Server user";
group = cfg.group;
extraGroups = [ "audio" ];
}
else {};
};
}

View file

@ -0,0 +1,73 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.slimserver;
in {
options = {
services.slimserver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable slimserver.
'';
};
package = mkOption {
type = types.package;
default = pkgs.slimserver;
defaultText = literalExpression "pkgs.slimserver";
description = "Slimserver package to use.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/slimserver";
description = ''
The directory where slimserver stores its state, tag cache,
playlists etc.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - slimserver slimserver - -"
];
systemd.services.slimserver = {
after = [ "network.target" ];
description = "Slim Server for Logitech Squeezebox Players";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "slimserver";
# Issue 40589: Disable broken image/video support (audio still works!)
ExecStart = "${cfg.package}/slimserver.pl --logdir ${cfg.dataDir}/logs --prefsdir ${cfg.dataDir}/prefs --cachedir ${cfg.dataDir}/cache --noimage --novideo";
};
};
users = {
users.slimserver = {
description = "Slimserver daemon user";
home = cfg.dataDir;
group = "slimserver";
isSystemUser = true;
};
groups.slimserver = {};
};
};
}

View file

@ -0,0 +1,323 @@
{ config, options, lib, pkgs, ... }:
with lib;
let
name = "snapserver";
cfg = config.services.snapserver;
# Using types.nullOr to inherit upstream defaults.
sampleFormat = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Default sample format.
'';
example = "48000:16:2";
};
codec = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Default audio compression method.
'';
example = "flac";
};
streamToOption = name: opt:
let
os = val:
optionalString (val != null) "${val}";
os' = prefix: val:
optionalString (val != null) (prefix + "${val}");
flatten = key: value:
"&${key}=${value}";
in
"--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name
+ concatStrings (mapAttrsToList flatten opt.query) + "\"";
optionalNull = val: ret:
optional (val != null) ret;
optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
# global options
++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
++ [ "--stream.port=${toString cfg.port}" ]
++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}"
++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}"
++ optional cfg.sendToMuted "--stream.send_to_muted"
# tcp json rpc
++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
++ optionals cfg.tcp.enable [
"--tcp.bind_to_address=${cfg.tcp.listenAddress}"
"--tcp.port=${toString cfg.tcp.port}" ]
# http json rpc
++ [ "--http.enabled=${toString cfg.http.enable}" ]
++ optionals cfg.http.enable [
"--http.bind_to_address=${cfg.http.listenAddress}"
"--http.port=${toString cfg.http.port}"
] ++ optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"");
in {
imports = [
(mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
];
###### interface
options = {
services.snapserver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable snapserver.
'';
};
listenAddress = mkOption {
type = types.str;
default = "::";
example = "0.0.0.0";
description = ''
The address where snapclients can connect.
'';
};
port = mkOption {
type = types.port;
default = 1704;
description = ''
The port that snapclients can connect to.
'';
};
openFirewall = mkOption {
type = types.bool;
# Make the behavior consistent with other services. Set the default to
# false and remove the accompanying warning after NixOS 22.05 is released.
default = true;
description = ''
Whether to automatically open the specified ports in the firewall.
'';
};
inherit sampleFormat;
inherit codec;
streamBuffer = mkOption {
type = with types; nullOr int;
default = null;
description = ''
Stream read (input) buffer in ms.
'';
example = 20;
};
buffer = mkOption {
type = with types; nullOr int;
default = null;
description = ''
Network buffer in ms.
'';
example = 1000;
};
sendToMuted = mkOption {
type = types.bool;
default = false;
description = ''
Send audio to muted clients.
'';
};
tcp.enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable the JSON-RPC via TCP.
'';
};
tcp.listenAddress = mkOption {
type = types.str;
default = "::";
example = "0.0.0.0";
description = ''
The address where the TCP JSON-RPC listens on.
'';
};
tcp.port = mkOption {
type = types.port;
default = 1705;
description = ''
The port where the TCP JSON-RPC listens on.
'';
};
http.enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable the JSON-RPC via HTTP.
'';
};
http.listenAddress = mkOption {
type = types.str;
default = "::";
example = "0.0.0.0";
description = ''
The address where the HTTP JSON-RPC listens on.
'';
};
http.port = mkOption {
type = types.port;
default = 1780;
description = ''
The port where the HTTP JSON-RPC listens on.
'';
};
http.docRoot = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Path to serve from the HTTP servers root.
'';
};
streams = mkOption {
type = with types; attrsOf (submodule {
options = {
location = mkOption {
type = types.oneOf [ types.path types.str ];
description = ''
For type <literal>pipe</literal> or <literal>file</literal>, the path to the pipe or file.
For type <literal>librespot</literal>, <literal>airplay</literal> or <literal>process</literal>, the path to the corresponding binary.
For type <literal>tcp</literal>, the <literal>host:port</literal> address to connect to or listen on.
For type <literal>meta</literal>, a list of stream names in the form <literal>/one/two/...</literal>. Don't forget the leading slash.
For type <literal>alsa</literal>, use an empty string.
'';
example = literalExpression ''
"/path/to/pipe"
"/path/to/librespot"
"192.168.1.2:4444"
"/MyTCP/Spotify/MyPipe"
'';
};
type = mkOption {
type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
default = "pipe";
description = ''
The type of input stream.
'';
};
query = mkOption {
type = attrsOf str;
default = {};
description = ''
Key-value pairs that convey additional parameters about a stream.
'';
example = literalExpression ''
# for type == "pipe":
{
mode = "create";
};
# for type == "process":
{
params = "--param1 --param2";
logStderr = "true";
};
# for type == "tcp":
{
mode = "client";
}
# for type == "alsa":
{
device = "hw:0,0";
}
'';
};
inherit sampleFormat;
inherit codec;
};
});
default = { default = {}; };
description = ''
The definition for an input source.
'';
example = literalExpression ''
{
mpd = {
type = "pipe";
location = "/run/snapserver/mpd";
sampleFormat = "48000:16:2";
codec = "pcm";
};
};
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
warnings =
# https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
'' else "") cfg.streams)
# Remove this warning after NixOS 22.05 is released.
++ optional (options.services.snapserver.openFirewall.highestPrio >= (mkOptionDefault null).priority) ''
services.snapserver.openFirewall will no longer default to true starting with NixOS 22.11.
Enable it explicitly if you need to control Snapserver remotely.
'';
systemd.services.snapserver = {
after = [ "network.target" ];
description = "Snapserver";
wantedBy = [ "multi-user.target" ];
before = [ "mpd.service" "mopidy.service" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
Type = "forking";
LimitRTPRIO = 50;
LimitRTTIME = "infinity";
NoNewPrivileges = true;
PIDFile = "/run/${name}/pid";
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
RestrictNamespaces = true;
RuntimeDirectory = name;
StateDirectory = name;
};
};
networking.firewall.allowedTCPPorts =
optionals cfg.openFirewall [ cfg.port ]
++ optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port
++ optional (cfg.openFirewall && cfg.http.enable) cfg.http.port;
};
meta = {
maintainers = with maintainers; [ tobim ];
};
}

View file

@ -0,0 +1,68 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.spotifyd;
toml = pkgs.formats.toml {};
warnConfig =
if cfg.config != ""
then lib.trace "Using the stringly typed .config attribute is discouraged. Use the TOML typed .settings attribute instead."
else id;
spotifydConf =
if cfg.settings != {}
then toml.generate "spotify.conf" cfg.settings
else warnConfig (pkgs.writeText "spotifyd.conf" cfg.config);
in
{
options = {
services.spotifyd = {
enable = mkEnableOption "spotifyd, a Spotify playing daemon";
config = mkOption {
default = "";
type = types.lines;
description = ''
(Deprecated) Configuration for Spotifyd. For syntax and directives, see
<link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
'';
};
settings = mkOption {
default = {};
type = toml.type;
example = { global.bitrate = 320; };
description = ''
Configuration for Spotifyd. For syntax and directives, see
<link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.config == "" || cfg.settings == {};
message = "At most one of the .config attribute and the .settings attribute may be set";
}
];
systemd.services.spotifyd = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "sound.target" ];
description = "spotifyd, a Spotify playing daemon";
environment.SHELL = "/bin/sh";
serviceConfig = {
ExecStart = "${pkgs.spotifyd}/bin/spotifyd --no-daemon --cache-path /var/cache/spotifyd --config-path ${spotifydConf}";
Restart = "always";
RestartSec = 12;
DynamicUser = true;
CacheDirectory = "spotifyd";
SupplementaryGroups = ["audio"];
};
};
};
meta.maintainers = [ maintainers.anderslundstedt ];
}

View file

@ -0,0 +1,46 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) mkEnableOption mkIf mkOption optionalString types;
dataDir = "/var/lib/squeezelite";
cfg = config.services.squeezelite;
pkg = if cfg.pulseAudio then pkgs.squeezelite-pulse else pkgs.squeezelite;
bin = "${pkg}/bin/${pkg.pname}";
in
{
###### interface
options.services.squeezelite = {
enable = mkEnableOption "Squeezelite, a software Squeezebox emulator";
pulseAudio = mkEnableOption "pulseaudio support";
extraArguments = mkOption {
default = "";
type = types.str;
description = ''
Additional command line arguments to pass to Squeezelite.
'';
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.squeezelite = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "sound.target" ];
description = "Software Squeezebox emulator";
serviceConfig = {
DynamicUser = true;
ExecStart = "${bin} -N ${dataDir}/player-name ${cfg.extraArguments}";
StateDirectory = builtins.baseNameOf dataDir;
SupplementaryGroups = "audio";
};
};
};
}

View file

@ -0,0 +1,57 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ympd;
in {
###### interface
options = {
services.ympd = {
enable = mkEnableOption "ympd, the MPD Web GUI";
webPort = mkOption {
type = types.either types.str types.port; # string for backwards compat
default = "8080";
description = "The port where ympd's web interface will be available.";
example = "ssl://8080:/path/to/ssl-private-key.pem";
};
mpd = {
host = mkOption {
type = types.str;
default = "localhost";
description = "The host where MPD is listening.";
};
port = mkOption {
type = types.int;
default = config.services.mpd.network.port;
defaultText = literalExpression "config.services.mpd.network.port";
description = "The port where MPD is listening.";
example = 6600;
};
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.ympd = {
description = "Standalone MPD Web GUI written in C";
wantedBy = [ "multi-user.target" ];
serviceConfig.ExecStart = "${pkgs.ympd}/bin/ympd --host ${cfg.mpd.host} --port ${toString cfg.mpd.port} --webport ${toString cfg.webPort} --user nobody";
};
};
}

View file

@ -0,0 +1,119 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) concatMapStringsSep concatStringsSep isInt isList literalExpression;
inherit (lib) mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkOption optional types;
cfg = config.services.automysqlbackup;
pkg = pkgs.automysqlbackup;
user = "automysqlbackup";
group = "automysqlbackup";
toStr = val:
if isList val then "( ${concatMapStringsSep " " (val: "'${val}'") val} )"
else if isInt val then toString val
else if true == val then "'yes'"
else if false == val then "'no'"
else "'${toString val}'";
configFile = pkgs.writeText "automysqlbackup.conf" ''
#version=${pkg.version}
# DONT'T REMOVE THE PREVIOUS VERSION LINE!
#
${concatStringsSep "\n" (mapAttrsToList (name: value: "CONFIG_${name}=${toStr value}") cfg.config)}
'';
in
{
# interface
options = {
services.automysqlbackup = {
enable = mkEnableOption "AutoMySQLBackup";
calendar = mkOption {
type = types.str;
default = "01:15:00";
description = ''
Configured when to run the backup service systemd unit (DayOfWeek Year-Month-Day Hour:Minute:Second).
'';
};
config = mkOption {
type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
default = {};
description = ''
automysqlbackup configuration. Refer to
<filename>''${pkgs.automysqlbackup}/etc/automysqlbackup.conf</filename>
for details on supported values.
'';
example = literalExpression ''
{
db_names = [ "nextcloud" "matomo" ];
table_exclude = [ "nextcloud.oc_users" "nextcloud.oc_whats_new" ];
mailcontent = "log";
mail_address = "admin@example.org";
}
'';
};
};
};
# implementation
config = mkIf cfg.enable {
assertions = [
{ assertion = !config.services.mysqlBackup.enable;
message = "Please choose one of services.mysqlBackup or services.automysqlbackup.";
}
];
services.automysqlbackup.config = mapAttrs (name: mkDefault) {
mysql_dump_username = user;
mysql_dump_host = "localhost";
mysql_dump_socket = "/run/mysqld/mysqld.sock";
backup_dir = "/var/backup/mysql";
db_exclude = [ "information_schema" "performance_schema" ];
mailcontent = "stdout";
mysql_dump_single_transaction = true;
};
systemd.timers.automysqlbackup = {
description = "automysqlbackup timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.calendar;
AccuracySec = "5m";
};
};
systemd.services.automysqlbackup = {
description = "automysqlbackup service";
serviceConfig = {
User = user;
Group = group;
ExecStart = "${pkg}/bin/automysqlbackup ${configFile}";
};
};
environment.systemPackages = [ pkg ];
users.users.${user} = {
group = group;
isSystemUser = true;
};
users.groups.${group} = { };
systemd.tmpfiles.rules = [
"d '${cfg.config.backup_dir}' 0750 ${user} ${group} - -"
];
services.mysql.ensureUsers = optional (config.services.mysql.enable && cfg.config.mysql_dump_host == "localhost") {
name = user;
ensurePermissions = { "*.*" = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT"; };
};
};
}

View file

@ -0,0 +1,578 @@
{ config, lib, pkgs, ... }:
# TODO: test configuration when building nixexpr (use -t parameter)
# TODO: support sqlite3 (it's deprecate?) and mysql
with lib;
let
libDir = "/var/lib/bacula";
fd_cfg = config.services.bacula-fd;
fd_conf = pkgs.writeText "bacula-fd.conf"
''
Client {
Name = "${fd_cfg.name}";
FDPort = ${toString fd_cfg.port};
WorkingDirectory = "${libDir}";
Pid Directory = "/run";
${fd_cfg.extraClientConfig}
}
${concatStringsSep "\n" (mapAttrsToList (name: value: ''
Director {
Name = "${name}";
Password = "${value.password}";
Monitor = "${value.monitor}";
}
'') fd_cfg.director)}
Messages {
Name = Standard;
syslog = all, !skipped, !restored
${fd_cfg.extraMessagesConfig}
}
'';
sd_cfg = config.services.bacula-sd;
sd_conf = pkgs.writeText "bacula-sd.conf"
''
Storage {
Name = "${sd_cfg.name}";
SDPort = ${toString sd_cfg.port};
WorkingDirectory = "${libDir}";
Pid Directory = "/run";
${sd_cfg.extraStorageConfig}
}
${concatStringsSep "\n" (mapAttrsToList (name: value: ''
Autochanger {
Name = "${name}";
Device = ${concatStringsSep ", " (map (a: "\"${a}\"") value.devices)};
Changer Device = "${value.changerDevice}";
Changer Command = "${value.changerCommand}";
${value.extraAutochangerConfig}
}
'') sd_cfg.autochanger)}
${concatStringsSep "\n" (mapAttrsToList (name: value: ''
Device {
Name = "${name}";
Archive Device = "${value.archiveDevice}";
Media Type = "${value.mediaType}";
${value.extraDeviceConfig}
}
'') sd_cfg.device)}
${concatStringsSep "\n" (mapAttrsToList (name: value: ''
Director {
Name = "${name}";
Password = "${value.password}";
Monitor = "${value.monitor}";
}
'') sd_cfg.director)}
Messages {
Name = Standard;
syslog = all, !skipped, !restored
${sd_cfg.extraMessagesConfig}
}
'';
dir_cfg = config.services.bacula-dir;
dir_conf = pkgs.writeText "bacula-dir.conf"
''
Director {
Name = "${dir_cfg.name}";
Password = "${dir_cfg.password}";
DirPort = ${toString dir_cfg.port};
Working Directory = "${libDir}";
Pid Directory = "/run/";
QueryFile = "${pkgs.bacula}/etc/query.sql";
${dir_cfg.extraDirectorConfig}
}
Catalog {
Name = "PostgreSQL";
dbname = "bacula";
user = "bacula";
}
Messages {
Name = Standard;
syslog = all, !skipped, !restored
${dir_cfg.extraMessagesConfig}
}
${dir_cfg.extraConfig}
'';
directorOptions = {...}:
{
options = {
password = mkOption {
type = types.str;
# TODO: required?
description = ''
Specifies the password that must be supplied for the default Bacula
Console to be authorized. The same password must appear in the
Director resource of the Console configuration file. For added
security, the password is never passed across the network but instead
a challenge response hash code created with the password. This
directive is required. If you have either /dev/random or bc on your
machine, Bacula will generate a random password during the
configuration process, otherwise it will be left blank and you must
manually supply it.
The password is plain text. It is not generated through any special
process but as noted above, it is better to use random text for
security reasons.
'';
};
monitor = mkOption {
type = types.enum [ "no" "yes" ];
default = "no";
example = "yes";
description = ''
If Monitor is set to <literal>no</literal>, this director will have
full access to this Storage daemon. If Monitor is set to
<literal>yes</literal>, this director will only be able to fetch the
current status of this Storage daemon.
Please note that if this director is being used by a Monitor, we
highly recommend to set this directive to yes to avoid serious
security problems.
'';
};
};
};
autochangerOptions = {...}:
{
options = {
changerDevice = mkOption {
type = types.str;
description = ''
The specified name-string must be the generic SCSI device name of the
autochanger that corresponds to the normal read/write Archive Device
specified in the Device resource. This generic SCSI device name
should be specified if you have an autochanger or if you have a
standard tape drive and want to use the Alert Command (see below).
For example, on Linux systems, for an Archive Device name of
<literal>/dev/nst0</literal>, you would specify
<literal>/dev/sg0</literal> for the Changer Device name. Depending
on your exact configuration, and the number of autochangers or the
type of autochanger, what you specify here can vary. This directive
is optional. See the Using AutochangersAutochangersChapter chapter of
this manual for more details of using this and the following
autochanger directives.
'';
};
changerCommand = mkOption {
type = types.str;
description = ''
The name-string specifies an external program to be called that will
automatically change volumes as required by Bacula. Normally, this
directive will be specified only in the AutoChanger resource, which
is then used for all devices. However, you may also specify the
different Changer Command in each Device resource. Most frequently,
you will specify the Bacula supplied mtx-changer script as follows:
<literal>"/path/mtx-changer %c %o %S %a %d"</literal>
and you will install the mtx on your system (found in the depkgs
release). An example of this command is in the default bacula-sd.conf
file. For more details on the substitution characters that may be
specified to configure your autochanger please see the
AutochangersAutochangersChapter chapter of this manual. For FreeBSD
users, you might want to see one of the several chio scripts in
examples/autochangers.
'';
default = "/etc/bacula/mtx-changer %c %o %S %a %d";
};
devices = mkOption {
description = "";
type = types.listOf types.str;
};
extraAutochangerConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Autochanger directive.
'';
example = ''
'';
};
};
};
deviceOptions = {...}:
{
options = {
archiveDevice = mkOption {
# TODO: required?
type = types.str;
description = ''
The specified name-string gives the system file name of the storage
device managed by this storage daemon. This will usually be the
device file name of a removable storage device (tape drive), for
example <literal>/dev/nst0</literal> or
<literal>/dev/rmt/0mbn</literal>. For a DVD-writer, it will be for
example <literal>/dev/hdc</literal>. It may also be a directory name
if you are archiving to disk storage. In this case, you must supply
the full absolute path to the directory. When specifying a tape
device, it is preferable that the "non-rewind" variant of the device
file name be given.
'';
};
mediaType = mkOption {
# TODO: required?
type = types.str;
description = ''
The specified name-string names the type of media supported by this
device, for example, <literal>DLT7000</literal>. Media type names are
arbitrary in that you set them to anything you want, but they must be
known to the volume database to keep track of which storage daemons
can read which volumes. In general, each different storage type
should have a unique Media Type associated with it. The same
name-string must appear in the appropriate Storage resource
definition in the Director's configuration file.
Even though the names you assign are arbitrary (i.e. you choose the
name you want), you should take care in specifying them because the
Media Type is used to determine which storage device Bacula will
select during restore. Thus you should probably use the same Media
Type specification for all drives where the Media can be freely
interchanged. This is not generally an issue if you have a single
Storage daemon, but it is with multiple Storage daemons, especially
if they have incompatible media.
For example, if you specify a Media Type of <literal>DDS-4</literal>
then during the restore, Bacula will be able to choose any Storage
Daemon that handles <literal>DDS-4</literal>. If you have an
autochanger, you might want to name the Media Type in a way that is
unique to the autochanger, unless you wish to possibly use the
Volumes in other drives. You should also ensure to have unique Media
Type names if the Media is not compatible between drives. This
specification is required for all devices.
In addition, if you are using disk storage, each Device resource will
generally have a different mount point or directory. In order for
Bacula to select the correct Device resource, each one must have a
unique Media Type.
'';
};
extraDeviceConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Device directive.
'';
example = ''
LabelMedia = yes
Random Access = no
AutomaticMount = no
RemovableMedia = no
MaximumOpenWait = 60
AlwaysOpen = no
'';
};
};
};
in {
options = {
services.bacula-fd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the Bacula File Daemon.
'';
};
name = mkOption {
default = "${config.networking.hostName}-fd";
defaultText = literalExpression ''"''${config.networking.hostName}-fd"'';
type = types.str;
description = ''
The client name that must be used by the Director when connecting.
Generally, it is a good idea to use a name related to the machine so
that error messages can be easily identified if you have multiple
Clients. This directive is required.
'';
};
port = mkOption {
default = 9102;
type = types.int;
description = ''
This specifies the port number on which the Client listens for
Director connections. It must agree with the FDPort specified in
the Client resource of the Director's configuration file.
'';
};
director = mkOption {
default = {};
description = ''
This option defines director resources in Bacula File Daemon.
'';
type = with types; attrsOf (submodule directorOptions);
};
extraClientConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Client directive.
'';
example = ''
Maximum Concurrent Jobs = 20;
Heartbeat Interval = 30;
'';
};
extraMessagesConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Messages directive.
'';
example = ''
console = all
'';
};
};
services.bacula-sd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Bacula Storage Daemon.
'';
};
name = mkOption {
default = "${config.networking.hostName}-sd";
defaultText = literalExpression ''"''${config.networking.hostName}-sd"'';
type = types.str;
description = ''
Specifies the Name of the Storage daemon.
'';
};
port = mkOption {
default = 9103;
type = types.int;
description = ''
Specifies port number on which the Storage daemon listens for
Director connections.
'';
};
director = mkOption {
default = {};
description = ''
This option defines Director resources in Bacula Storage Daemon.
'';
type = with types; attrsOf (submodule directorOptions);
};
device = mkOption {
default = {};
description = ''
This option defines Device resources in Bacula Storage Daemon.
'';
type = with types; attrsOf (submodule deviceOptions);
};
autochanger = mkOption {
default = {};
description = ''
This option defines Autochanger resources in Bacula Storage Daemon.
'';
type = with types; attrsOf (submodule autochangerOptions);
};
extraStorageConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Storage directive.
'';
example = ''
Maximum Concurrent Jobs = 20;
Heartbeat Interval = 30;
'';
};
extraMessagesConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Messages directive.
'';
example = ''
console = all
'';
};
};
services.bacula-dir = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Bacula Director Daemon.
'';
};
name = mkOption {
default = "${config.networking.hostName}-dir";
defaultText = literalExpression ''"''${config.networking.hostName}-dir"'';
type = types.str;
description = ''
The director name used by the system administrator. This directive is
required.
'';
};
port = mkOption {
default = 9101;
type = types.int;
description = ''
Specify the port (a positive integer) on which the Director daemon
will listen for Bacula Console connections. This same port number
must be specified in the Director resource of the Console
configuration file. The default is 9101, so normally this directive
need not be specified. This directive should not be used if you
specify DirAddresses (N.B plural) directive.
'';
};
password = mkOption {
# TODO: required?
type = types.str;
description = ''
Specifies the password that must be supplied for a Director.
'';
};
extraMessagesConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Messages directive.
'';
example = ''
console = all
'';
};
extraDirectorConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration to be passed in Director directive.
'';
example = ''
Maximum Concurrent Jobs = 20;
Heartbeat Interval = 30;
'';
};
extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration for Bacula Director Daemon.
'';
example = ''
TODO
'';
};
};
};
config = mkIf (fd_cfg.enable || sd_cfg.enable || dir_cfg.enable) {
systemd.services.bacula-fd = mkIf fd_cfg.enable {
after = [ "network.target" ];
description = "Bacula File Daemon";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.bacula ];
serviceConfig = {
ExecStart = "${pkgs.bacula}/sbin/bacula-fd -f -u root -g bacula -c ${fd_conf}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
LogsDirectory = "bacula";
StateDirectory = "bacula";
};
};
systemd.services.bacula-sd = mkIf sd_cfg.enable {
after = [ "network.target" ];
description = "Bacula Storage Daemon";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.bacula ];
serviceConfig = {
ExecStart = "${pkgs.bacula}/sbin/bacula-sd -f -u bacula -g bacula -c ${sd_conf}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
LogsDirectory = "bacula";
StateDirectory = "bacula";
};
};
services.postgresql.enable = dir_cfg.enable == true;
systemd.services.bacula-dir = mkIf dir_cfg.enable {
after = [ "network.target" "postgresql.service" ];
description = "Bacula Director Daemon";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.bacula ];
serviceConfig = {
ExecStart = "${pkgs.bacula}/sbin/bacula-dir -f -u bacula -g bacula -c ${dir_conf}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
LogsDirectory = "bacula";
StateDirectory = "bacula";
};
preStart = ''
if ! test -e "${libDir}/db-created"; then
${pkgs.postgresql}/bin/createuser --no-superuser --no-createdb --no-createrole bacula
#${pkgs.postgresql}/bin/createdb --owner bacula bacula
# populate DB
${pkgs.bacula}/etc/create_bacula_database postgresql
${pkgs.bacula}/etc/make_bacula_tables postgresql
${pkgs.bacula}/etc/grant_bacula_privileges postgresql
touch "${libDir}/db-created"
else
${pkgs.bacula}/etc/update_bacula_tables postgresql || true
fi
'';
};
environment.systemPackages = [ pkgs.bacula ];
users.users.bacula = {
group = "bacula";
uid = config.ids.uids.bacula;
home = "${libDir}";
createHome = true;
description = "Bacula Daemons user";
shell = "${pkgs.bash}/bin/bash";
};
users.groups.bacula.gid = config.ids.gids.bacula;
};
}

View file

@ -0,0 +1,730 @@
{ config, lib, pkgs, ... }:
with lib;
let
isLocalPath = x:
builtins.substring 0 1 x == "/" # absolute path
|| builtins.substring 0 1 x == "." # relative path
|| builtins.match "[.*:.*]" == null; # not machine:path
mkExcludeFile = cfg:
# Write each exclude pattern to a new line
pkgs.writeText "excludefile" (concatStringsSep "\n" cfg.exclude);
mkKeepArgs = cfg:
# If cfg.prune.keep e.g. has a yearly attribute,
# its content is passed on as --keep-yearly
concatStringsSep " "
(mapAttrsToList (x: y: "--keep-${x}=${toString y}") cfg.prune.keep);
mkBackupScript = cfg: ''
on_exit()
{
exitStatus=$?
# Reset the EXIT handler, or else we're called again on 'exit' below
trap - EXIT
${cfg.postHook}
exit $exitStatus
}
trap 'on_exit' INT TERM QUIT EXIT
archiveName="${if cfg.archiveBaseName == null then "" else cfg.archiveBaseName + "-"}$(date ${cfg.dateFormat})"
archiveSuffix="${optionalString cfg.appendFailedSuffix ".failed"}"
${cfg.preHook}
'' + optionalString cfg.doInit ''
# Run borg init if the repo doesn't exist yet
if ! borg list $extraArgs > /dev/null; then
borg init $extraArgs \
--encryption ${cfg.encryption.mode} \
$extraInitArgs
${cfg.postInit}
fi
'' + ''
(
set -o pipefail
${optionalString (cfg.dumpCommand != null) ''${escapeShellArg cfg.dumpCommand} | \''}
borg create $extraArgs \
--compression ${cfg.compression} \
--exclude-from ${mkExcludeFile cfg} \
$extraCreateArgs \
"::$archiveName$archiveSuffix" \
${if cfg.paths == null then "-" else escapeShellArgs cfg.paths}
)
'' + optionalString cfg.appendFailedSuffix ''
borg rename $extraArgs \
"::$archiveName$archiveSuffix" "$archiveName"
'' + ''
${cfg.postCreate}
'' + optionalString (cfg.prune.keep != { }) ''
borg prune $extraArgs \
${mkKeepArgs cfg} \
${optionalString (cfg.prune.prefix != null) "--prefix ${escapeShellArg cfg.prune.prefix} \\"}
$extraPruneArgs
${cfg.postPrune}
'';
mkPassEnv = cfg: with cfg.encryption;
if passCommand != null then
{ BORG_PASSCOMMAND = passCommand; }
else if passphrase != null then
{ BORG_PASSPHRASE = passphrase; }
else { };
mkBackupService = name: cfg:
let
userHome = config.users.users.${cfg.user}.home;
in nameValuePair "borgbackup-job-${name}" {
description = "BorgBackup job ${name}";
path = with pkgs; [
borgbackup openssh
];
script = mkBackupScript cfg;
serviceConfig = {
User = cfg.user;
Group = cfg.group;
# Only run when no other process is using CPU or disk
CPUSchedulingPolicy = "idle";
IOSchedulingClass = "idle";
ProtectSystem = "strict";
ReadWritePaths =
[ "${userHome}/.config/borg" "${userHome}/.cache/borg" ]
++ cfg.readWritePaths
# Borg needs write access to repo if it is not remote
++ optional (isLocalPath cfg.repo) cfg.repo;
PrivateTmp = cfg.privateTmp;
};
environment = {
BORG_REPO = cfg.repo;
inherit (cfg) extraArgs extraInitArgs extraCreateArgs extraPruneArgs;
} // (mkPassEnv cfg) // cfg.environment;
};
mkBackupTimers = name: cfg:
nameValuePair "borgbackup-job-${name}" {
description = "BorgBackup job ${name} timer";
wantedBy = [ "timers.target" ];
timerConfig = {
Persistent = cfg.persistentTimer;
OnCalendar = cfg.startAt;
};
# if remote-backup wait for network
after = optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target";
};
# utility function around makeWrapper
mkWrapperDrv = {
original, name, set ? {}
}:
pkgs.runCommand "${name}-wrapper" {
buildInputs = [ pkgs.makeWrapper ];
} (with lib; ''
makeWrapper "${original}" "$out/bin/${name}" \
${concatStringsSep " \\\n " (mapAttrsToList (name: value: ''--set ${name} "${value}"'') set)}
'');
mkBorgWrapper = name: cfg: mkWrapperDrv {
original = "${pkgs.borgbackup}/bin/borg";
name = "borg-job-${name}";
set = { BORG_REPO = cfg.repo; } // (mkPassEnv cfg) // cfg.environment;
};
# Paths listed in ReadWritePaths must exist before service is started
mkActivationScript = name: cfg:
let
install = "install -o ${cfg.user} -g ${cfg.group}";
in
nameValuePair "borgbackup-job-${name}" (stringAfter [ "users" ] (''
# Ensure that the home directory already exists
# We can't assert createHome == true because that's not the case for root
cd "${config.users.users.${cfg.user}.home}"
${install} -d .config/borg
${install} -d .cache/borg
'' + optionalString (isLocalPath cfg.repo && !cfg.removableDevice) ''
${install} -d ${escapeShellArg cfg.repo}
''));
mkPassAssertion = name: cfg: {
assertion = with cfg.encryption;
mode != "none" -> passCommand != null || passphrase != null;
message =
"passCommand or passphrase has to be specified because"
+ '' borgbackup.jobs.${name}.encryption != "none"'';
};
mkRepoService = name: cfg:
nameValuePair "borgbackup-repo-${name}" {
description = "Create BorgBackup repository ${name} directory";
script = ''
mkdir -p ${escapeShellArg cfg.path}
chown ${cfg.user}:${cfg.group} ${escapeShellArg cfg.path}
'';
serviceConfig = {
# The service's only task is to ensure that the specified path exists
Type = "oneshot";
};
wantedBy = [ "multi-user.target" ];
};
mkAuthorizedKey = cfg: appendOnly: key:
let
# Because of the following line, clients do not need to specify an absolute repo path
cdCommand = "cd ${escapeShellArg cfg.path}";
restrictedArg = "--restrict-to-${if cfg.allowSubRepos then "path" else "repository"} .";
appendOnlyArg = optionalString appendOnly "--append-only";
quotaArg = optionalString (cfg.quota != null) "--storage-quota ${cfg.quota}";
serveCommand = "borg serve ${restrictedArg} ${appendOnlyArg} ${quotaArg}";
in
''command="${cdCommand} && ${serveCommand}",restrict ${key}'';
mkUsersConfig = name: cfg: {
users.${cfg.user} = {
openssh.authorizedKeys.keys =
(map (mkAuthorizedKey cfg false) cfg.authorizedKeys
++ map (mkAuthorizedKey cfg true) cfg.authorizedKeysAppendOnly);
useDefaultShell = true;
group = cfg.group;
isSystemUser = true;
};
groups.${cfg.group} = { };
};
mkKeysAssertion = name: cfg: {
assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeysAppendOnly != [ ];
message =
"borgbackup.repos.${name} does not make sense"
+ " without at least one public key";
};
mkSourceAssertions = name: cfg: {
assertion = count isNull [ cfg.dumpCommand cfg.paths ] == 1;
message = ''
Exactly one of borgbackup.jobs.${name}.paths or borgbackup.jobs.${name}.dumpCommand
must be set.
'';
};
mkRemovableDeviceAssertions = name: cfg: {
assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice;
message = ''
borgbackup.repos.${name}: repo isn't a local path, thus it can't be a removable device!
'';
};
in {
meta.maintainers = with maintainers; [ dotlambda ];
meta.doc = ./borgbackup.xml;
###### interface
options.services.borgbackup.jobs = mkOption {
description = ''
Deduplicating backups using BorgBackup.
Adding a job will cause a borg-job-NAME wrapper to be added
to your system path, so that you can perform maintenance easily.
See also the chapter about BorgBackup in the NixOS manual.
'';
default = { };
example = literalExpression ''
{ # for a local backup
rootBackup = {
paths = "/";
exclude = [ "/nix" ];
repo = "/path/to/local/repo";
encryption = {
mode = "repokey";
passphrase = "secret";
};
compression = "auto,lzma";
startAt = "weekly";
};
}
{ # Root backing each day up to a remote backup server. We assume that you have
# * created a password less key: ssh-keygen -N "" -t ed25519 -f /path/to/ssh_key
# best practices are: use -t ed25519, /path/to = /run/keys
# * the passphrase is in the file /run/keys/borgbackup_passphrase
# * you have initialized the repository manually
paths = [ "/etc" "/home" ];
exclude = [ "/nix" "'**/.cache'" ];
doInit = false;
repo = "user3@arep.repo.borgbase.com:repo";
encryption = {
mode = "repokey-blake2";
passCommand = "cat /path/to/passphrase";
};
environment = { BORG_RSH = "ssh -i /path/to/ssh_key"; };
compression = "auto,lzma";
startAt = "daily";
};
'';
type = types.attrsOf (types.submodule (let globalConfig = config; in
{ name, config, ... }: {
options = {
paths = mkOption {
type = with types; nullOr (coercedTo str lib.singleton (listOf str));
default = null;
description = ''
Path(s) to back up.
Mutually exclusive with <option>dumpCommand</option>.
'';
example = "/home/user";
};
dumpCommand = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Backup the stdout of this program instead of filesystem paths.
Mutually exclusive with <option>paths</option>.
'';
example = "/path/to/createZFSsend.sh";
};
repo = mkOption {
type = types.str;
description = "Remote or local repository to back up to.";
example = "user@machine:/path/to/repo";
};
removableDevice = mkOption {
type = types.bool;
default = false;
description = "Whether the repo (which must be local) is a removable device.";
};
archiveBaseName = mkOption {
type = types.nullOr (types.strMatching "[^/{}]+");
default = "${globalConfig.networking.hostName}-${name}";
defaultText = literalExpression ''"''${config.networking.hostName}-<name>"'';
description = ''
How to name the created archives. A timestamp, whose format is
determined by <option>dateFormat</option>, will be appended. The full
name can be modified at runtime (<literal>$archiveName</literal>).
Placeholders like <literal>{hostname}</literal> must not be used.
Use <literal>null</literal> for no base name.
'';
};
dateFormat = mkOption {
type = types.str;
description = ''
Arguments passed to <command>date</command>
to create a timestamp suffix for the archive name.
'';
default = "+%Y-%m-%dT%H:%M:%S";
example = "-u +%s";
};
startAt = mkOption {
type = with types; either str (listOf str);
default = "daily";
description = ''
When or how often the backup should run.
Must be in the format described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
If you do not want the backup to start
automatically, use <literal>[ ]</literal>.
It will generate a systemd service borgbackup-job-NAME.
You may trigger it manually via systemctl restart borgbackup-job-NAME.
'';
};
persistentTimer = mkOption {
default = false;
type = types.bool;
example = true;
description = ''
Set the <literal>persistentTimer</literal> option for the
<citerefentry><refentrytitle>systemd.timer</refentrytitle>
<manvolnum>5</manvolnum></citerefentry>
which triggers the backup immediately if the last trigger
was missed (e.g. if the system was powered down).
'';
};
user = mkOption {
type = types.str;
description = ''
The user <command>borg</command> is run as.
User or group need read permission
for the specified <option>paths</option>.
'';
default = "root";
};
group = mkOption {
type = types.str;
description = ''
The group borg is run as. User or group needs read permission
for the specified <option>paths</option>.
'';
default = "root";
};
encryption.mode = mkOption {
type = types.enum [
"repokey" "keyfile"
"repokey-blake2" "keyfile-blake2"
"authenticated" "authenticated-blake2"
"none"
];
description = ''
Encryption mode to use. Setting a mode
other than <literal>"none"</literal> requires
you to specify a <option>passCommand</option>
or a <option>passphrase</option>.
'';
example = "repokey-blake2";
};
encryption.passCommand = mkOption {
type = with types; nullOr str;
description = ''
A command which prints the passphrase to stdout.
Mutually exclusive with <option>passphrase</option>.
'';
default = null;
example = "cat /path/to/passphrase_file";
};
encryption.passphrase = mkOption {
type = with types; nullOr str;
description = ''
The passphrase the backups are encrypted with.
Mutually exclusive with <option>passCommand</option>.
If you do not want the passphrase to be stored in the
world-readable Nix store, use <option>passCommand</option>.
'';
default = null;
};
compression = mkOption {
# "auto" is optional,
# compression mode must be given,
# compression level is optional
type = types.strMatching "none|(auto,)?(lz4|zstd|zlib|lzma)(,[[:digit:]]{1,2})?";
description = ''
Compression method to use. Refer to
<command>borg help compression</command>
for all available options.
'';
default = "lz4";
example = "auto,lzma";
};
exclude = mkOption {
type = with types; listOf str;
description = ''
Exclude paths matching any of the given patterns. See
<command>borg help patterns</command> for pattern syntax.
'';
default = [ ];
example = [
"/home/*/.cache"
"/nix"
];
};
readWritePaths = mkOption {
type = with types; listOf path;
description = ''
By default, borg cannot write anywhere on the system but
<literal>$HOME/.config/borg</literal> and <literal>$HOME/.cache/borg</literal>.
If, for example, your preHook script needs to dump files
somewhere, put those directories here.
'';
default = [ ];
example = [
"/var/backup/mysqldump"
];
};
privateTmp = mkOption {
type = types.bool;
description = ''
Set the <literal>PrivateTmp</literal> option for
the systemd-service. Set to false if you need sockets
or other files from global /tmp.
'';
default = true;
};
doInit = mkOption {
type = types.bool;
description = ''
Run <command>borg init</command> if the
specified <option>repo</option> does not exist.
You should set this to <literal>false</literal>
if the repository is located on an external drive
that might not always be mounted.
'';
default = true;
};
appendFailedSuffix = mkOption {
type = types.bool;
description = ''
Append a <literal>.failed</literal> suffix
to the archive name, which is only removed if
<command>borg create</command> has a zero exit status.
'';
default = true;
};
prune.keep = mkOption {
# Specifying e.g. `prune.keep.yearly = -1`
# means there is no limit of yearly archives to keep
# The regex is for use with e.g. --keep-within 1y
type = with types; attrsOf (either int (strMatching "[[:digit:]]+[Hdwmy]"));
description = ''
Prune a repository by deleting all archives not matching any of the
specified retention options. See <command>borg help prune</command>
for the available options.
'';
default = { };
example = literalExpression ''
{
within = "1d"; # Keep all archives from the last day
daily = 7;
weekly = 4;
monthly = -1; # Keep at least one archive for each month
}
'';
};
prune.prefix = mkOption {
type = types.nullOr (types.str);
description = ''
Only consider archive names starting with this prefix for pruning.
By default, only archives created by this job are considered.
Use <literal>""</literal> or <literal>null</literal> to consider all archives.
'';
default = config.archiveBaseName;
defaultText = literalExpression "archiveBaseName";
};
environment = mkOption {
type = with types; attrsOf str;
description = ''
Environment variables passed to the backup script.
You can for example specify which SSH key to use.
'';
default = { };
example = { BORG_RSH = "ssh -i /path/to/key"; };
};
preHook = mkOption {
type = types.lines;
description = ''
Shell commands to run before the backup.
This can for example be used to mount file systems.
'';
default = "";
example = ''
# To add excluded paths at runtime
extraCreateArgs="$extraCreateArgs --exclude /some/path"
'';
};
postInit = mkOption {
type = types.lines;
description = ''
Shell commands to run after <command>borg init</command>.
'';
default = "";
};
postCreate = mkOption {
type = types.lines;
description = ''
Shell commands to run after <command>borg create</command>. The name
of the created archive is stored in <literal>$archiveName</literal>.
'';
default = "";
};
postPrune = mkOption {
type = types.lines;
description = ''
Shell commands to run after <command>borg prune</command>.
'';
default = "";
};
postHook = mkOption {
type = types.lines;
description = ''
Shell commands to run just before exit. They are executed
even if a previous command exits with a non-zero exit code.
The latter is available as <literal>$exitStatus</literal>.
'';
default = "";
};
extraArgs = mkOption {
type = types.str;
description = ''
Additional arguments for all <command>borg</command> calls the
service has. Handle with care.
'';
default = "";
example = "--remote-path=/path/to/borg";
};
extraInitArgs = mkOption {
type = types.str;
description = ''
Additional arguments for <command>borg init</command>.
Can also be set at runtime using <literal>$extraInitArgs</literal>.
'';
default = "";
example = "--append-only";
};
extraCreateArgs = mkOption {
type = types.str;
description = ''
Additional arguments for <command>borg create</command>.
Can also be set at runtime using <literal>$extraCreateArgs</literal>.
'';
default = "";
example = "--stats --checkpoint-interval 600";
};
extraPruneArgs = mkOption {
type = types.str;
description = ''
Additional arguments for <command>borg prune</command>.
Can also be set at runtime using <literal>$extraPruneArgs</literal>.
'';
default = "";
example = "--save-space";
};
};
}
));
};
options.services.borgbackup.repos = mkOption {
description = ''
Serve BorgBackup repositories to given public SSH keys,
restricting their access to the repository only.
See also the chapter about BorgBackup in the NixOS manual.
Also, clients do not need to specify the absolute path when accessing the repository,
i.e. <literal>user@machine:.</literal> is enough. (Note colon and dot.)
'';
default = { };
type = types.attrsOf (types.submodule (
{ ... }: {
options = {
path = mkOption {
type = types.path;
description = ''
Where to store the backups. Note that the directory
is created automatically, with correct permissions.
'';
default = "/var/lib/borgbackup";
};
user = mkOption {
type = types.str;
description = ''
The user <command>borg serve</command> is run as.
User or group needs write permission
for the specified <option>path</option>.
'';
default = "borg";
};
group = mkOption {
type = types.str;
description = ''
The group <command>borg serve</command> is run as.
User or group needs write permission
for the specified <option>path</option>.
'';
default = "borg";
};
authorizedKeys = mkOption {
type = with types; listOf str;
description = ''
Public SSH keys that are given full write access to this repository.
You should use a different SSH key for each repository you write to, because
the specified keys are restricted to running <command>borg serve</command>
and can only access this single repository.
'';
default = [ ];
};
authorizedKeysAppendOnly = mkOption {
type = with types; listOf str;
description = ''
Public SSH keys that can only be used to append new data (archives) to the repository.
Note that archives can still be marked as deleted and are subsequently removed from disk
upon accessing the repo with full write access, e.g. when pruning.
'';
default = [ ];
};
allowSubRepos = mkOption {
type = types.bool;
description = ''
Allow clients to create repositories in subdirectories of the
specified <option>path</option>. These can be accessed using
<literal>user@machine:path/to/subrepo</literal>. Note that a
<option>quota</option> applies to repositories independently.
Therefore, if this is enabled, clients can create multiple
repositories and upload an arbitrary amount of data.
'';
default = false;
};
quota = mkOption {
# See the definition of parse_file_size() in src/borg/helpers/parseformat.py
type = with types; nullOr (strMatching "[[:digit:].]+[KMGTP]?");
description = ''
Storage quota for the repository. This quota is ensured for all
sub-repositories if <option>allowSubRepos</option> is enabled
but not for the overall storage space used.
'';
default = null;
example = "100G";
};
};
}
));
};
###### implementation
config = mkIf (with config.services.borgbackup; jobs != { } || repos != { })
(with config.services.borgbackup; {
assertions =
mapAttrsToList mkPassAssertion jobs
++ mapAttrsToList mkKeysAssertion repos
++ mapAttrsToList mkSourceAssertions jobs
++ mapAttrsToList mkRemovableDeviceAssertions jobs;
system.activationScripts = mapAttrs' mkActivationScript jobs;
systemd.services =
# A job named "foo" is mapped to systemd.services.borgbackup-job-foo
mapAttrs' mkBackupService jobs
# A repo named "foo" is mapped to systemd.services.borgbackup-repo-foo
// mapAttrs' mkRepoService repos;
# A job named "foo" is mapped to systemd.timers.borgbackup-job-foo
# only generate the timer if interval (startAt) is set
systemd.timers = mapAttrs' mkBackupTimers (filterAttrs (_: cfg: cfg.startAt != []) jobs);
users = mkMerge (mapAttrsToList mkUsersConfig repos);
environment.systemPackages = with pkgs; [ borgbackup ] ++ (mapAttrsToList mkBorgWrapper jobs);
});
}

View file

@ -0,0 +1,209 @@
<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-borgbase">
<title>BorgBackup</title>
<para>
<emphasis>Source:</emphasis>
<filename>modules/services/backup/borgbackup.nix</filename>
</para>
<para>
<emphasis>Upstream documentation:</emphasis>
<link xlink:href="https://borgbackup.readthedocs.io/"/>
</para>
<para>
<link xlink:href="https://www.borgbackup.org/">BorgBackup</link> (short: Borg)
is a deduplicating backup program. Optionally, it supports compression and
authenticated encryption.
</para>
<para>
The main goal of Borg is to provide an efficient and secure way to backup
data. The data deduplication technique used makes Borg suitable for daily
backups since only changes are stored. The authenticated encryption technique
makes it suitable for backups to not fully trusted targets.
</para>
<section xml:id="module-services-backup-borgbackup-configuring">
<title>Configuring</title>
<para>
A complete list of options for the Borgbase module may be found
<link linkend="opt-services.borgbackup.jobs">here</link>.
</para>
</section>
<section xml:id="opt-services-backup-borgbackup-local-directory">
<title>Basic usage for a local backup</title>
<para>
A very basic configuration for backing up to a locally accessible directory
is:
<programlisting>
{
opt.services.borgbackup.jobs = {
{ rootBackup = {
paths = "/";
exclude = [ "/nix" "/path/to/local/repo" ];
repo = "/path/to/local/repo";
doInit = true;
encryption = {
mode = "repokey";
passphrase = "secret";
};
compression = "auto,lzma";
startAt = "weekly";
};
}
};
}</programlisting>
</para>
<warning>
<para>
If you do not want the passphrase to be stored in the world-readable
Nix store, use passCommand. You find an example below.
</para>
</warning>
</section>
<section xml:id="opt-services-backup-create-server">
<title>Create a borg backup server</title>
<para>You should use a different SSH key for each repository you write to,
because the specified keys are restricted to running borg serve and can only
access this single repository. You need the output of the generate pub file.
</para>
<para>
<screen>
<prompt># </prompt>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
<prompt># </prompt>cat /run/keys/id_ed25519_my_borg_repo
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos</screen>
</para>
<para>
Add the following snippet to your NixOS configuration:
<programlisting>
{
services.borgbackup.repos = {
my_borg_repo = {
authorizedKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos"
] ;
path = "/var/lib/my_borg_repo" ;
};
};
}</programlisting>
</para>
</section>
<section xml:id="opt-services-backup-borgbackup-remote-server">
<title>Backup to the borg repository server</title>
<para>The following NixOS snippet creates an hourly backup to the service
(on the host nixos) as created in the section above. We assume
that you have stored a secret passphrasse in the file
<code>/run/keys/borgbackup_passphrase</code>, which should be only
accessible by root
</para>
<para>
<programlisting>
{
services.borgbackup.jobs = {
backupToLocalServer = {
paths = [ "/etc/nixos" ];
doInit = true;
repo = "borg@nixos:." ;
encryption = {
mode = "repokey-blake2";
passCommand = "cat /run/keys/borgbackup_passphrase";
};
environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_my_borg_repo"; };
compression = "auto,lzma";
startAt = "hourly";
};
};
};</programlisting>
</para>
<para>The following few commands (run as root) let you test your backup.
<programlisting>
> nixos-rebuild switch
...restarting the following units: polkit.service
> systemctl restart borgbackup-job-backupToLocalServer
> sleep 10
> systemctl restart borgbackup-job-backupToLocalServer
> export BORG_PASSPHRASE=topSecrect
> borg list --rsh='ssh -i /run/keys/id_ed25519_my_borg_repo' borg@nixos:.
nixos-backupToLocalServer-2020-03-30T21:46:17 Mon, 2020-03-30 21:46:19 [84feb97710954931ca384182f5f3cb90665f35cef214760abd7350fb064786ac]
nixos-backupToLocalServer-2020-03-30T21:46:30 Mon, 2020-03-30 21:46:32 [e77321694ecd160ca2228611747c6ad1be177d6e0d894538898de7a2621b6e68]</programlisting>
</para>
</section>
<section xml:id="opt-services-backup-borgbackup-borgbase">
<title>Backup to a hosting service</title>
<para>
Several companies offer <link
xlink:href="https://www.borgbackup.org/support/commercial.html">(paid)
hosting services</link> for Borg repositories.
</para>
<para>
To backup your home directory to borgbase you have to:
</para>
<itemizedlist>
<listitem>
<para>
Generate a SSH key without a password, to access the remote server. E.g.
</para>
<para>
<programlisting>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_borgbase</programlisting>
</para>
</listitem>
<listitem>
<para>
Create the repository on the server by following the instructions for your
hosting server.
</para>
</listitem>
<listitem>
<para>
Initialize the repository on the server. Eg.
<programlisting>
sudo borg init --encryption=repokey-blake2 \
-rsh "ssh -i /run/keys/id_ed25519_borgbase" \
zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo</programlisting>
</para>
</listitem>
<listitem>
<para>Add it to your NixOS configuration, e.g.
<programlisting>
{
services.borgbackup.jobs = {
my_Remote_Backup = {
paths = [ "/" ];
exclude = [ "/nix" "'**/.cache'" ];
repo = "zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo";
encryption = {
mode = "repokey-blake2";
passCommand = "cat /run/keys/borgbackup_passphrase";
};
BORG_RSH = "ssh -i /run/keys/id_ed25519_borgbase";
compression = "auto,lzma";
startAt = "daily";
};
};
}}</programlisting>
</para>
</listitem>
</itemizedlist>
</section>
<section xml:id="opt-services-backup-borgbackup-vorta">
<title>Vorta backup client for the desktop</title>
<para>
Vorta is a backup client for macOS and Linux desktops. It integrates the
mighty BorgBackup with your desktop environment to protect your data from
disk failure, ransomware and theft.
</para>
<para>
It can be installed in NixOS e.g. by adding <package>pkgs.vorta</package>
to <xref linkend="opt-environment.systemPackages" />.
</para>
<para>
Details about using Vorta can be found under <link
xlink:href="https://vorta.borgbase.com/usage">https://vorta.borgbase.com
</link>.
</para>
</section>
</chapter>

View file

@ -0,0 +1,58 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.borgmatic;
settingsFormat = pkgs.formats.yaml { };
cfgfile = settingsFormat.generate "config.yaml" cfg.settings;
in {
options.services.borgmatic = {
enable = mkEnableOption "borgmatic";
settings = mkOption {
description = ''
See https://torsion.org/borgmatic/docs/reference/configuration/
'';
type = types.submodule {
freeformType = settingsFormat.type;
options.location = {
source_directories = mkOption {
type = types.listOf types.str;
description = ''
List of source directories to backup (required). Globs and
tildes are expanded.
'';
example = [ "/home" "/etc" "/var/log/syslog*" ];
};
repositories = mkOption {
type = types.listOf types.str;
description = ''
Paths to local or remote repositories (required). Tildes are
expanded. Multiple repositories are backed up to in
sequence. Borg placeholders can be used. See the output of
"borg help placeholders" for details. See ssh_command for
SSH options like identity file or port. If systemd service
is used, then add local repository paths in the systemd
service file to the ReadWritePaths list.
'';
example = [
"user@backupserver:sourcehostname.borg"
"user@backupserver:{fqdn}"
];
};
};
};
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.borgmatic ];
environment.etc."borgmatic/config.yaml".source = cfgfile;
systemd.packages = [ pkgs.borgmatic ];
};
}

View file

@ -0,0 +1,244 @@
{ config, pkgs, lib, ... }:
let
inherit (lib)
concatMapStringsSep
concatStringsSep
filterAttrs
flatten
isAttrs
isString
literalExpression
mapAttrs'
mapAttrsToList
mkIf
mkOption
optionalString
partition
typeOf
types
;
cfg = config.services.btrbk;
sshEnabled = cfg.sshAccess != [ ];
serviceEnabled = cfg.instances != { };
attr2Lines = attr:
let
pairs = mapAttrsToList (name: value: { inherit name value; }) attr;
isSubsection = value:
if isAttrs value then true
else if isString value then false
else throw "invalid type in btrbk config ${typeOf value}";
sortedPairs = partition (x: isSubsection x.value) pairs;
in
flatten (
# non subsections go first
(
map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
)
++ # subsections go last
(
map
(
pair:
mapAttrsToList
(
childname: value:
[ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
)
pair.value
)
sortedPairs.right
)
)
;
addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
mkConfigFile = settings: concatStringsSep "\n" (attr2Lines (addDefaults settings));
mkTestedConfigFile = name: settings:
let
configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
in
pkgs.runCommand "btrbk-${name}-tested.conf" { } ''
mkdir foo
cp ${configFile} $out
if (set +o pipefail; ${pkgs.btrbk}/bin/btrbk -c $out ls foo 2>&1 | grep $out);
then
echo btrbk configuration is invalid
cat $out
exit 1
fi;
'';
in
{
meta.maintainers = with lib.maintainers; [ oxalica ];
options = {
services.btrbk = {
extraPackages = mkOption {
description = "Extra packages for btrbk, like compression utilities for <literal>stream_compress</literal>";
type = types.listOf types.package;
default = [ ];
example = literalExpression "[ pkgs.xz ]";
};
niceness = mkOption {
description = "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive.";
type = types.ints.between (-20) 19;
default = 10;
};
ioSchedulingClass = mkOption {
description = "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle.";
type = types.enum [ "idle" "best-effort" "realtime" ];
default = "best-effort";
};
instances = mkOption {
description = "Set of btrbk instances. The instance named <literal>btrbk</literal> is the default one.";
type = with types;
attrsOf (
submodule {
options = {
onCalendar = mkOption {
type = types.nullOr types.str;
default = "daily";
description = ''
How often this btrbk instance is started. See systemd.time(7) for more information about the format.
Setting it to null disables the timer, thus this instance can only be started manually.
'';
};
settings = mkOption {
type = let t = types.attrsOf (types.either types.str (t // { description = "instances of this type recursively"; })); in t;
default = { };
example = {
snapshot_preserve_min = "2d";
snapshot_preserve = "14d";
volume = {
"/mnt/btr_pool" = {
target = "/mnt/btr_backup/mylaptop";
subvolume = {
"rootfs" = { };
"home" = { snapshot_create = "always"; };
};
};
};
};
description = "configuration options for btrbk. Nested attrsets translate to subsections.";
};
};
}
);
default = { };
};
sshAccess = mkOption {
description = "SSH keys that should be able to make or push snapshots on this system remotely with btrbk";
type = with types; listOf (
submodule {
options = {
key = mkOption {
type = str;
description = "SSH public key allowed to login as user <literal>btrbk</literal> to run remote backups.";
};
roles = mkOption {
type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]);
example = [ "source" "info" "send" ];
description = "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details";
};
};
}
);
default = [ ];
};
};
};
config = mkIf (sshEnabled || serviceEnabled) {
environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
security.sudo.extraRules = [
{
users = [ "btrbk" ];
commands = [
{ command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
{ command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
{ command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
# for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
{ command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
{ command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
];
}
];
users.users.btrbk = {
isSystemUser = true;
# ssh needs a home directory
home = "/var/lib/btrbk";
createHome = true;
shell = "${pkgs.bash}/bin/bash";
group = "btrbk";
openssh.authorizedKeys.keys = map
(
v:
let
options = concatMapStringsSep " " (x: "--" + x) v.roles;
ioniceClass = {
"idle" = 3;
"best-effort" = 2;
"realtime" = 1;
}.${cfg.ioSchedulingClass};
in
''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
)
cfg.sshAccess;
};
users.groups.btrbk = { };
systemd.tmpfiles.rules = [
"d /var/lib/btrbk 0750 btrbk btrbk"
"d /var/lib/btrbk/.ssh 0700 btrbk btrbk"
"f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new"
];
environment.etc = mapAttrs'
(
name: instance: {
name = "btrbk/${name}.conf";
value.source = mkTestedConfigFile name instance.settings;
}
)
cfg.instances;
systemd.services = mapAttrs'
(
name: _: {
name = "btrbk-${name}";
value = {
description = "Takes BTRFS snapshots and maintains retention policies.";
unitConfig.Documentation = "man:btrbk(1)";
path = [ "/run/wrappers" ] ++ cfg.extraPackages;
serviceConfig = {
User = "btrbk";
Group = "btrbk";
Type = "oneshot";
ExecStart = "${pkgs.btrbk}/bin/btrbk -c /etc/btrbk/${name}.conf run";
Nice = cfg.niceness;
IOSchedulingClass = cfg.ioSchedulingClass;
StateDirectory = "btrbk";
};
};
}
)
cfg.instances;
systemd.timers = mapAttrs'
(
name: instance: {
name = "btrbk-${name}";
value = {
description = "Timer to take BTRFS snapshots and maintain retention policies.";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = instance.onCalendar;
AccuracySec = "10min";
Persistent = true;
};
};
}
)
(filterAttrs (name: instance: instance.onCalendar != null)
cfg.instances);
};
}

View file

@ -0,0 +1,86 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.duplicati;
in
{
options = {
services.duplicati = {
enable = mkEnableOption "Duplicati";
port = mkOption {
default = 8200;
type = types.int;
description = ''
Port serving the web interface
'';
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/duplicati";
description = ''
The directory where Duplicati stores its data files.
<note><para>
If left as the default value this directory will automatically be created
before the Duplicati server starts, otherwise you are responsible for ensuring
the directory exists with appropriate ownership and permissions.
</para></note>
'';
};
interface = mkOption {
default = "127.0.0.1";
type = types.str;
description = ''
Listening interface for the web UI
Set it to "any" to listen on all available interfaces
'';
};
user = mkOption {
default = "duplicati";
type = types.str;
description = ''
Duplicati runs as it's own user. It will only be able to backup world-readable files.
Run as root with special care.
'';
};
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.duplicati ];
systemd.services.duplicati = {
description = "Duplicati backup";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = mkMerge [
{
User = cfg.user;
Group = "duplicati";
ExecStart = "${pkgs.duplicati}/bin/duplicati-server --webservice-interface=${cfg.interface} --webservice-port=${toString cfg.port} --server-datafolder=${cfg.dataDir}";
Restart = "on-failure";
}
(mkIf (cfg.dataDir == "/var/lib/duplicati") {
StateDirectory = "duplicati";
})
];
};
users.users = lib.optionalAttrs (cfg.user == "duplicati") {
duplicati = {
uid = config.ids.uids.duplicati;
home = cfg.dataDir;
group = "duplicati";
};
};
users.groups.duplicati.gid = config.ids.gids.duplicati;
};
}

View file

@ -0,0 +1,196 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.duplicity;
stateDirectory = "/var/lib/duplicity";
localTarget =
if hasPrefix "file://" cfg.targetUrl
then removePrefix "file://" cfg.targetUrl else null;
in
{
options.services.duplicity = {
enable = mkEnableOption "backups with duplicity";
root = mkOption {
type = types.path;
default = "/";
description = ''
Root directory to backup.
'';
};
include = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/home" ];
description = ''
List of paths to include into the backups. See the FILE SELECTION
section in <citerefentry><refentrytitle>duplicity</refentrytitle>
<manvolnum>1</manvolnum></citerefentry> for details on the syntax.
'';
};
exclude = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
List of paths to exclude from backups. See the FILE SELECTION section in
<citerefentry><refentrytitle>duplicity</refentrytitle>
<manvolnum>1</manvolnum></citerefentry> for details on the syntax.
'';
};
targetUrl = mkOption {
type = types.str;
example = "s3://host:port/prefix";
description = ''
Target url to backup to. See the URL FORMAT section in
<citerefentry><refentrytitle>duplicity</refentrytitle>
<manvolnum>1</manvolnum></citerefentry> for supported urls.
'';
};
secretFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path of a file containing secrets (gpg passphrase, access key...) in
the format of EnvironmentFile as described by
<citerefentry><refentrytitle>systemd.exec</refentrytitle>
<manvolnum>5</manvolnum></citerefentry>. For example:
<programlisting>
PASSPHRASE=<replaceable>...</replaceable>
AWS_ACCESS_KEY_ID=<replaceable>...</replaceable>
AWS_SECRET_ACCESS_KEY=<replaceable>...</replaceable>
</programlisting>
'';
};
frequency = mkOption {
type = types.nullOr types.str;
default = "daily";
description = ''
Run duplicity with the given frequency (see
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry> for the format).
If null, do not run automatically.
'';
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--backend-retry-delay" "100" ];
description = ''
Extra command-line flags passed to duplicity. See
<citerefentry><refentrytitle>duplicity</refentrytitle>
<manvolnum>1</manvolnum></citerefentry>.
'';
};
fullIfOlderThan = mkOption {
type = types.str;
default = "never";
example = "1M";
description = ''
If <literal>"never"</literal> (the default) always do incremental
backups (the first backup will be a full backup, of course). If
<literal>"always"</literal> always do full backups. Otherwise, this
must be a string representing a duration. Full backups will be made
when the latest full backup is older than this duration. If this is not
the case, an incremental backup is performed.
'';
};
cleanup = {
maxAge = mkOption {
type = types.nullOr types.str;
default = null;
example = "6M";
description = ''
If non-null, delete all backup sets older than the given time. Old backup sets
will not be deleted if backup sets newer than time depend on them.
'';
};
maxFull = mkOption {
type = types.nullOr types.int;
default = null;
example = 2;
description = ''
If non-null, delete all backups sets that are older than the count:th last full
backup (in other words, keep the last count full backups and
associated incremental sets).
'';
};
maxIncr = mkOption {
type = types.nullOr types.int;
default = null;
example = 1;
description = ''
If non-null, delete incremental sets of all backups sets that are
older than the count:th last full backup (in other words, keep only
old full backups and not their increments).
'';
};
};
};
config = mkIf cfg.enable {
systemd = {
services.duplicity = {
description = "backup files with duplicity";
environment.HOME = stateDirectory;
script =
let
target = escapeShellArg cfg.targetUrl;
extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
dup = "${pkgs.duplicity}/bin/duplicity";
in
''
set -x
${dup} cleanup ${target} --force ${extra}
${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-inc-of-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs (
[ cfg.root cfg.targetUrl ]
++ concatMap (p: [ "--include" p ]) cfg.include
++ concatMap (p: [ "--exclude" p ]) cfg.exclude
++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ])
)} ${extra}
'';
serviceConfig = {
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
StateDirectory = baseNameOf stateDirectory;
} // optionalAttrs (localTarget != null) {
ReadWritePaths = localTarget;
} // optionalAttrs (cfg.secretFile != null) {
EnvironmentFile = cfg.secretFile;
};
} // optionalAttrs (cfg.frequency != null) {
startAt = cfg.frequency;
};
tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
};
assertions = singleton {
# Duplicity will fail if the last file selection option is an include. It
# is not always possible to detect but this simple case can be caught.
assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
message = ''
Duplicity will fail if you only specify included paths ("Because the
default is to include all files, the expression is redundant. Exiting
because this probably isn't what you meant.")
'';
};
};
}

View file

@ -0,0 +1,130 @@
{ config, lib, pkgs, ... }:
with lib;
let
inherit (pkgs) mariadb gzip;
cfg = config.services.mysqlBackup;
defaultUser = "mysqlbackup";
backupScript = ''
set -o pipefail
failed=""
${concatMapStringsSep "\n" backupDatabaseScript cfg.databases}
if [ -n "$failed" ]; then
echo "Backup of database(s) failed:$failed"
exit 1
fi
'';
backupDatabaseScript = db: ''
dest="${cfg.location}/${db}.gz"
if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
mv $dest.tmp $dest
echo "Backed up to $dest"
else
echo "Failed to back up to $dest"
rm -f $dest.tmp
failed="$failed ${db}"
fi
'';
in
{
options = {
services.mysqlBackup = {
enable = mkEnableOption "MySQL backups";
calendar = mkOption {
type = types.str;
default = "01:15:00";
description = ''
Configured when to run the backup service systemd unit (DayOfWeek Year-Month-Day Hour:Minute:Second).
'';
};
user = mkOption {
type = types.str;
default = defaultUser;
description = ''
User to be used to perform backup.
'';
};
databases = mkOption {
default = [];
type = types.listOf types.str;
description = ''
List of database names to dump.
'';
};
location = mkOption {
type = types.path;
default = "/var/backup/mysql";
description = ''
Location to put the gzipped MySQL database dumps.
'';
};
singleTransaction = mkOption {
default = false;
type = types.bool;
description = ''
Whether to create database dump in a single transaction
'';
};
};
};
config = mkIf cfg.enable {
users.users = optionalAttrs (cfg.user == defaultUser) {
${defaultUser} = {
isSystemUser = true;
createHome = false;
home = cfg.location;
group = "nogroup";
};
};
services.mysql.ensureUsers = [{
name = cfg.user;
ensurePermissions = with lib;
let
privs = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES";
grant = db: nameValuePair "${db}.*" privs;
in
listToAttrs (map grant cfg.databases);
}];
systemd = {
timers.mysql-backup = {
description = "Mysql backup timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.calendar;
AccuracySec = "5m";
Unit = "mysql-backup.service";
};
};
services.mysql-backup = {
description = "MySQL backup service";
enable = true;
serviceConfig = {
Type = "oneshot";
User = cfg.user;
};
script = backupScript;
};
tmpfiles.rules = [
"d ${cfg.location} 0700 ${cfg.user} - - -"
];
};
};
}

View file

@ -0,0 +1,164 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.postgresqlBackup;
postgresqlBackupService = db: dumpCmd:
let
compressSuffixes = {
"none" = "";
"gzip" = ".gz";
"zstd" = ".zstd";
};
compressSuffix = getAttr cfg.compression compressSuffixes;
compressCmd = getAttr cfg.compression {
"none" = "cat";
"gzip" = "${pkgs.gzip}/bin/gzip -c";
"zstd" = "${pkgs.zstd}/bin/zstd -c";
};
mkSqlPath = prefix: suffix: "${cfg.location}/${db}${prefix}.sql${suffix}";
curFile = mkSqlPath "" compressSuffix;
prevFile = mkSqlPath ".prev" compressSuffix;
prevFiles = map (mkSqlPath ".prev") (attrValues compressSuffixes);
inProgressFile = mkSqlPath ".in-progress" compressSuffix;
in {
enable = true;
description = "Backup of ${db} database(s)";
requires = [ "postgresql.service" ];
path = [ pkgs.coreutils config.services.postgresql.package ];
script = ''
set -e -o pipefail
umask 0077 # ensure backup is only readable by postgres user
if [ -e ${curFile} ]; then
rm -f ${toString prevFiles}
mv ${curFile} ${prevFile}
fi
${dumpCmd} \
| ${compressCmd} \
> ${inProgressFile}
mv ${inProgressFile} ${curFile}
'';
serviceConfig = {
Type = "oneshot";
User = "postgres";
};
startAt = cfg.startAt;
};
in {
imports = [
(mkRemovedOptionModule [ "services" "postgresqlBackup" "period" ] ''
A systemd timer is now used instead of cron.
The starting time can be configured via <literal>services.postgresqlBackup.startAt</literal>.
'')
];
options = {
services.postgresqlBackup = {
enable = mkEnableOption "PostgreSQL dumps";
startAt = mkOption {
default = "*-*-* 01:15:00";
type = with types; either (listOf str) str;
description = ''
This option defines (see <literal>systemd.time</literal> for format) when the
databases should be dumped.
The default is to update at 01:15 (at night) every day.
'';
};
backupAll = mkOption {
default = cfg.databases == [];
defaultText = literalExpression "services.postgresqlBackup.databases == []";
type = lib.types.bool;
description = ''
Backup all databases using pg_dumpall.
This option is mutual exclusive to
<literal>services.postgresqlBackup.databases</literal>.
The resulting backup dump will have the name all.sql.gz.
This option is the default if no databases are specified.
'';
};
databases = mkOption {
default = [];
type = types.listOf types.str;
description = ''
List of database names to dump.
'';
};
location = mkOption {
default = "/var/backup/postgresql";
type = types.path;
description = ''
Path of directory where the PostgreSQL database dumps will be placed.
'';
};
pgdumpOptions = mkOption {
type = types.separatedString " ";
default = "-C";
description = ''
Command line options for pg_dump. This options is not used
if <literal>config.services.postgresqlBackup.backupAll</literal> is enabled.
Note that config.services.postgresqlBackup.backupAll is also active,
when no databases where specified.
'';
};
compression = mkOption {
type = types.enum ["none" "gzip" "zstd"];
default = "gzip";
description = ''
The type of compression to use on the generated database dump.
'';
};
};
};
config = mkMerge [
{
assertions = [{
assertion = cfg.backupAll -> cfg.databases == [];
message = "config.services.postgresqlBackup.backupAll cannot be used together with config.services.postgresqlBackup.databases";
}];
}
(mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.location}' 0700 postgres - - -"
];
})
(mkIf (cfg.enable && cfg.backupAll) {
systemd.services.postgresqlBackup =
postgresqlBackupService "all" "pg_dumpall";
})
(mkIf (cfg.enable && !cfg.backupAll) {
systemd.services = listToAttrs (map (db:
let
cmd = "pg_dump ${cfg.pgdumpOptions} ${db}";
in {
name = "postgresqlBackup-${db}";
value = postgresqlBackupService db cmd;
}) cfg.databases);
})
];
}

View file

@ -0,0 +1,204 @@
{ config, lib, pkgs, ... }:
with lib;
let
receiverSubmodule = {
options = {
postgresqlPackage = mkOption {
type = types.package;
example = literalExpression "pkgs.postgresql_11";
description = ''
PostgreSQL package to use.
'';
};
directory = mkOption {
type = types.path;
example = literalExpression "/mnt/pg_wal/main/";
description = ''
Directory to write the output to.
'';
};
statusInterval = mkOption {
type = types.int;
default = 10;
description = ''
Specifies the number of seconds between status packets sent back to the server.
This allows for easier monitoring of the progress from server.
A value of zero disables the periodic status updates completely,
although an update will still be sent when requested by the server, to avoid timeout disconnect.
'';
};
slot = mkOption {
type = types.str;
default = "";
example = "some_slot_name";
description = ''
Require <command>pg_receivewal</command> to use an existing replication slot (see
<link xlink:href="https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS">Section 26.2.6 of the PostgreSQL manual</link>).
When this option is used, <command>pg_receivewal</command> will report a flush position to the server,
indicating when each segment has been synchronized to disk so that the server can remove that segment if it is not otherwise needed.
When the replication client of <command>pg_receivewal</command> is configured on the server as a synchronous standby,
then using a replication slot will report the flush position to the server, but only when a WAL file is closed.
Therefore, that configuration will cause transactions on the primary to wait for a long time and effectively not work satisfactorily.
The option <option>synchronous</option> must be specified in addition to make this work correctly.
'';
};
synchronous = mkOption {
type = types.bool;
default = false;
description = ''
Flush the WAL data to disk immediately after it has been received.
Also send a status packet back to the server immediately after flushing, regardless of <option>statusInterval</option>.
This option should be specified if the replication client of <command>pg_receivewal</command> is configured on the server as a synchronous standby,
to ensure that timely feedback is sent to the server.
'';
};
compress = mkOption {
type = types.ints.between 0 9;
default = 0;
description = ''
Enables gzip compression of write-ahead logs, and specifies the compression level
(<literal>0</literal> through <literal>9</literal>, <literal>0</literal> being no compression and <literal>9</literal> being best compression).
The suffix <literal>.gz</literal> will automatically be added to all filenames.
This option requires PostgreSQL >= 10.
'';
};
connection = mkOption {
type = types.str;
example = "postgresql://user@somehost";
description = ''
Specifies parameters used to connect to the server, as a connection string.
See <link xlink:href="https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING">Section 34.1.1 of the PostgreSQL manual</link> for more information.
Because <command>pg_receivewal</command> doesn't connect to any particular database in the cluster,
database name in the connection string will be ignored.
'';
};
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = literalExpression ''
[
"--no-sync"
]
'';
description = ''
A list of extra arguments to pass to the <command>pg_receivewal</command> command.
'';
};
environment = mkOption {
type = with types; attrsOf str;
default = { };
example = literalExpression ''
{
PGPASSFILE = "/private/passfile";
PGSSLMODE = "require";
}
'';
description = ''
Environment variables passed to the service.
Usable parameters are listed in <link xlink:href="https://www.postgresql.org/docs/current/libpq-envars.html">Section 34.14 of the PostgreSQL manual</link>.
'';
};
};
};
in {
options = {
services.postgresqlWalReceiver = {
receivers = mkOption {
type = with types; attrsOf (submodule receiverSubmodule);
default = { };
example = literalExpression ''
{
main = {
postgresqlPackage = pkgs.postgresql_11;
directory = /mnt/pg_wal/main/;
slot = "main_wal_receiver";
connection = "postgresql://user@somehost";
};
}
'';
description = ''
PostgreSQL WAL receivers.
Stream write-ahead logs from a PostgreSQL server using <command>pg_receivewal</command> (formerly <command>pg_receivexlog</command>).
See <link xlink:href="https://www.postgresql.org/docs/current/app-pgreceivewal.html">the man page</link> for more information.
'';
};
};
};
config = let
receivers = config.services.postgresqlWalReceiver.receivers;
in mkIf (receivers != { }) {
users = {
users.postgres = {
uid = config.ids.uids.postgres;
group = "postgres";
description = "PostgreSQL server user";
};
groups.postgres = {
gid = config.ids.gids.postgres;
};
};
assertions = concatLists (attrsets.mapAttrsToList (name: config: [
{
assertion = config.compress > 0 -> versionAtLeast config.postgresqlPackage.version "10";
message = "Invalid configuration for WAL receiver \"${name}\": compress requires PostgreSQL version >= 10.";
}
]) receivers);
systemd.tmpfiles.rules = mapAttrsToList (name: config: ''
d ${escapeShellArg config.directory} 0750 postgres postgres - -
'') receivers;
systemd.services = with attrsets; mapAttrs' (name: config: nameValuePair "postgresql-wal-receiver-${name}" {
description = "PostgreSQL WAL receiver (${name})";
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 0; # retry forever, useful in case of network disruption
serviceConfig = {
User = "postgres";
Group = "postgres";
KillSignal = "SIGINT";
Restart = "always";
RestartSec = 60;
};
inherit (config) environment;
script = let
receiverCommand = postgresqlPackage:
if (versionAtLeast postgresqlPackage.version "10")
then "${postgresqlPackage}/bin/pg_receivewal"
else "${postgresqlPackage}/bin/pg_receivexlog";
in ''
${receiverCommand config.postgresqlPackage} \
--no-password \
--directory=${escapeShellArg config.directory} \
--status-interval=${toString config.statusInterval} \
--dbname=${escapeShellArg config.connection} \
${optionalString (config.compress > 0) "--compress=${toString config.compress}"} \
${optionalString (config.slot != "") "--slot=${escapeShellArg config.slot}"} \
${optionalString config.synchronous "--synchronous"} \
${concatStringsSep " " config.extraArgs}
'';
}) receivers;
};
meta.maintainers = with maintainers; [ pacien ];
}

View file

@ -0,0 +1,111 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.restic.server;
in
{
meta.maintainers = [ maintainers.bachp ];
options.services.restic.server = {
enable = mkEnableOption "Restic REST Server";
listenAddress = mkOption {
default = ":8000";
example = "127.0.0.1:8080";
type = types.str;
description = "Listen on a specific IP address and port.";
};
dataDir = mkOption {
default = "/var/lib/restic";
type = types.path;
description = "The directory for storing the restic repository.";
};
appendOnly = mkOption {
default = false;
type = types.bool;
description = ''
Enable append only mode.
This mode allows creation of new backups but prevents deletion and modification of existing backups.
This can be useful when backing up systems that have a potential of being hacked.
'';
};
privateRepos = mkOption {
default = false;
type = types.bool;
description = ''
Enable private repos.
Grants access only when a subdirectory with the same name as the user is specified in the repository URL.
'';
};
prometheus = mkOption {
default = false;
type = types.bool;
description = "Enable Prometheus metrics at /metrics.";
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Extra commandline options to pass to Restic REST server.
'';
};
package = mkOption {
default = pkgs.restic-rest-server;
defaultText = literalExpression "pkgs.restic-rest-server";
type = types.package;
description = "Restic REST server package to use.";
};
};
config = mkIf cfg.enable {
systemd.services.restic-rest-server = {
description = "Restic REST Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''
${cfg.package}/bin/rest-server \
--listen ${cfg.listenAddress} \
--path ${cfg.dataDir} \
${optionalString cfg.appendOnly "--append-only"} \
${optionalString cfg.privateRepos "--private-repos"} \
${optionalString cfg.prometheus "--prometheus"} \
${escapeShellArgs cfg.extraFlags} \
'';
Type = "simple";
User = "restic";
Group = "restic";
# Security hardening
ReadWritePaths = [ cfg.dataDir ];
PrivateTmp = true;
ProtectSystem = "strict";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
PrivateDevices = true;
};
};
systemd.tmpfiles.rules = mkIf cfg.privateRepos [
"f ${cfg.dataDir}/.htpasswd 0700 restic restic -"
];
users.users.restic = {
group = "restic";
home = cfg.dataDir;
createHome = true;
uid = config.ids.uids.restic;
};
users.groups.restic.gid = config.ids.uids.restic;
};
}

View file

@ -0,0 +1,334 @@
{ config, lib, pkgs, utils, ... }:
with lib;
let
# Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
inherit (utils.systemdUtils.unitOptions) unitOption;
in
{
options.services.restic.backups = mkOption {
description = ''
Periodic backups to create with Restic.
'';
type = types.attrsOf (types.submodule ({ config, name, ... }: {
options = {
passwordFile = mkOption {
type = types.str;
description = ''
Read the repository password from a file.
'';
example = "/etc/nixos/restic-password";
};
environmentFile = mkOption {
type = with types; nullOr str;
# added on 2021-08-28, s3CredentialsFile should
# be removed in the future (+ remember the warning)
default = config.s3CredentialsFile;
description = ''
file containing the credentials to access the repository, in the
format of an EnvironmentFile as described by systemd.exec(5)
'';
};
s3CredentialsFile = mkOption {
type = with types; nullOr str;
default = null;
description = ''
file containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
for an S3-hosted repository, in the format of an EnvironmentFile
as described by systemd.exec(5)
'';
};
rcloneOptions = mkOption {
type = with types; nullOr (attrsOf (oneOf [ str bool ]));
default = null;
description = ''
Options to pass to rclone to control its behavior.
See <link xlink:href="https://rclone.org/docs/#options"/> for
available options. When specifying option names, strip the
leading <literal>--</literal>. To set a flag such as
<literal>--drive-use-trash</literal>, which does not take a value,
set the value to the Boolean <literal>true</literal>.
'';
example = {
bwlimit = "10M";
drive-use-trash = "true";
};
};
rcloneConfig = mkOption {
type = with types; nullOr (attrsOf (oneOf [ str bool ]));
default = null;
description = ''
Configuration for the rclone remote being used for backup.
See the remote's specific options under rclone's docs at
<link xlink:href="https://rclone.org/docs/"/>. When specifying
option names, use the "config" name specified in the docs.
For example, to set <literal>--b2-hard-delete</literal> for a B2
remote, use <literal>hard_delete = true</literal> in the
attribute set.
Warning: Secrets set in here will be world-readable in the Nix
store! Consider using the <literal>rcloneConfigFile</literal>
option instead to specify secret values separately. Note that
options set here will override those set in the config file.
'';
example = {
type = "b2";
account = "xxx";
key = "xxx";
hard_delete = true;
};
};
rcloneConfigFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Path to the file containing rclone configuration. This file
must contain configuration for the remote specified in this backup
set and also must be readable by root. Options set in
<literal>rcloneConfig</literal> will override those set in this
file.
'';
};
repository = mkOption {
type = with types; nullOr str;
default = null;
description = ''
repository to backup to.
'';
example = "sftp:backup@192.168.1.100:/backups/${name}";
};
repositoryFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Path to the file containing the repository location to backup to.
'';
};
paths = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = ''
Which paths to backup. If null or an empty array, no
backup command will be run. This can be used to create a
prune-only job.
'';
example = [
"/var/lib/postgresql"
"/home/user/backup"
];
};
timerConfig = mkOption {
type = types.attrsOf unitOption;
default = {
OnCalendar = "daily";
};
description = ''
When to run the backup. See man systemd.timer for details.
'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
};
user = mkOption {
type = types.str;
default = "root";
description = ''
As which user the backup should run.
'';
example = "postgresql";
};
extraBackupArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra arguments passed to restic backup.
'';
example = [
"--exclude-file=/etc/nixos/restic-ignore"
];
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra extended options to be passed to the restic --option flag.
'';
example = [
"sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
];
};
initialize = mkOption {
type = types.bool;
default = false;
description = ''
Create the repository if it doesn't exist.
'';
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of options (--keep-* et al.) for 'restic forget
--prune', to automatically prune old snapshots. The
'forget' command is run *after* the 'backup' command, so
keep that in mind when constructing the --keep-* options.
'';
example = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
dynamicFilesFrom = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that produces a list of files to back up. The
results of this command are given to the '--files-from'
option.
'';
example = "find /home/matt/git -type d -name .git";
};
backupPrepareCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run before starting the backup process.
'';
};
backupCleanupCommand = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A script that must run after finishing the backup process.
'';
};
};
}));
default = { };
example = {
localbackup = {
paths = [ "/home" ];
repository = "/mnt/backup-hdd";
passwordFile = "/etc/nixos/secrets/restic-password";
initialize = true;
};
remotebackup = {
paths = [ "/home" ];
repository = "sftp:backup@host:/backups/home";
passwordFile = "/etc/nixos/secrets/restic-password";
extraOptions = [
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
];
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
};
};
};
config = {
warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
systemd.services =
mapAttrs'
(name: backup:
let
extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
filesFromTmpFile = "/run/restic-backups-${name}/includes";
backupPaths =
if (backup.dynamicFilesFrom == null)
then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
else "--files-from ${filesFromTmpFile}";
pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
(resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts))
(resticCmd + " check")
];
# Helper functions for rclone remotes
rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
in
nameValuePair "restic-backups-${name}" ({
environment = {
RESTIC_PASSWORD_FILE = backup.passwordFile;
RESTIC_REPOSITORY = backup.repository;
RESTIC_REPOSITORY_FILE = backup.repositoryFile;
} // optionalAttrs (backup.rcloneOptions != null) (mapAttrs'
(name: value:
nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
)
backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
RCLONE_CONFIG = backup.rcloneConfigFile;
} // optionalAttrs (backup.rcloneConfig != null) (mapAttrs'
(name: value:
nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
)
backup.rcloneConfig);
path = [ pkgs.openssh ];
restartIfChanged = false;
serviceConfig = {
Type = "oneshot";
ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
++ pruneCmd;
User = backup.user;
RuntimeDirectory = "restic-backups-${name}";
CacheDirectory = "restic-backups-${name}";
CacheDirectoryMode = "0700";
} // optionalAttrs (backup.environmentFile != null) {
EnvironmentFile = backup.environmentFile;
};
} // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null || backup.backupPrepareCommand != null) {
preStart = ''
${optionalString (backup.backupPrepareCommand != null) ''
${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand}
''}
${optionalString (backup.initialize) ''
${resticCmd} snapshots || ${resticCmd} init
''}
${optionalString (backup.dynamicFilesFrom != null) ''
${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
''}
'';
} // optionalAttrs (backup.dynamicFilesFrom != null || backup.backupCleanupCommand != null) {
postStart = ''
${optionalString (backup.backupCleanupCommand != null) ''
${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand}
''}
${optionalString (backup.dynamicFilesFrom != null) ''
rm ${filesFromTmpFile}
''}
'';
})
)
config.services.restic.backups;
systemd.timers =
mapAttrs'
(name: backup: nameValuePair "restic-backups-${name}" {
wantedBy = [ "timers.target" ];
timerConfig = backup.timerConfig;
})
config.services.restic.backups;
};
}

View file

@ -0,0 +1,75 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.rsnapshot;
cfgfile = pkgs.writeText "rsnapshot.conf" ''
config_version 1.2
cmd_cp ${pkgs.coreutils}/bin/cp
cmd_rm ${pkgs.coreutils}/bin/rm
cmd_rsync ${pkgs.rsync}/bin/rsync
cmd_ssh ${pkgs.openssh}/bin/ssh
cmd_logger ${pkgs.inetutils}/bin/logger
cmd_du ${pkgs.coreutils}/bin/du
cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff
lockfile /run/rsnapshot.pid
link_dest 1
${cfg.extraConfig}
'';
in
{
options = {
services.rsnapshot = {
enable = mkEnableOption "rsnapshot backups";
enableManualRsnapshot = mkOption {
description = "Whether to enable manual usage of the rsnapshot command with this module.";
default = true;
type = types.bool;
};
extraConfig = mkOption {
default = "";
example = ''
retains hourly 24
retain daily 365
backup /home/ localhost/
'';
type = types.lines;
description = ''
rsnapshot configuration option in addition to the defaults from
rsnapshot and this module.
Note that tabs are required to separate option arguments, and
directory names require trailing slashes.
The "extra" in the option name might be a little misleading right
now, as it is required to get a functional configuration.
'';
};
cronIntervals = mkOption {
default = {};
example = { hourly = "0 * * * *"; daily = "50 21 * * *"; };
type = types.attrsOf types.str;
description = ''
Periodicity at which intervals should be run by cron.
Note that the intervals also have to exist in configuration
as retain options.
'';
};
};
};
config = mkIf cfg.enable (mkMerge [
{
services.cron.systemCronJobs =
mapAttrsToList (interval: time: "${time} root ${pkgs.rsnapshot}/bin/rsnapshot -c ${cfgfile} ${interval}") cfg.cronIntervals;
}
(mkIf cfg.enableManualRsnapshot {
environment.systemPackages = [ pkgs.rsnapshot ];
environment.etc."rsnapshot.conf".source = cfgfile;
})
]);
}

View file

@ -0,0 +1,204 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.sanoid;
datasetSettingsType = with types;
(attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
description = "dataset/template options";
};
commonOptions = {
hourly = mkOption {
description = "Number of hourly snapshots.";
type = with types; nullOr ints.unsigned;
default = null;
};
daily = mkOption {
description = "Number of daily snapshots.";
type = with types; nullOr ints.unsigned;
default = null;
};
monthly = mkOption {
description = "Number of monthly snapshots.";
type = with types; nullOr ints.unsigned;
default = null;
};
yearly = mkOption {
description = "Number of yearly snapshots.";
type = with types; nullOr ints.unsigned;
default = null;
};
autoprune = mkOption {
description = "Whether to automatically prune old snapshots.";
type = with types; nullOr bool;
default = null;
};
autosnap = mkOption {
description = "Whether to automatically take snapshots.";
type = with types; nullOr bool;
default = null;
};
};
datasetOptions = rec {
use_template = mkOption {
description = "Names of the templates to use for this dataset.";
type = types.listOf (types.str // {
check = (types.enum (attrNames cfg.templates)).check;
description = "configured template name";
});
default = [ ];
};
useTemplate = use_template;
recursive = mkOption {
description = ''
Whether to recursively snapshot dataset children.
You can also set this to <literal>"zfs"</literal> to handle datasets
recursively in an atomic way without the possibility to
override settings for child datasets.
'';
type = with types; oneOf [ bool (enum [ "zfs" ]) ];
default = false;
};
process_children_only = mkOption {
description = "Whether to only snapshot child datasets if recursing.";
type = types.bool;
default = false;
};
processChildrenOnly = process_children_only;
};
# Extract unique dataset names
datasets = unique (attrNames cfg.datasets);
# Function to build "zfs allow" and "zfs unallow" commands for the
# filesystems we've delegated permissions to.
buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
# Here we explicitly use the booted system to guarantee the stable API needed by ZFS
"-+/run/booted-system/sw/bin/zfs"
zfsAction
"sanoid"
(concatStringsSep "," permissions)
dataset
];
configFile =
let
mkValueString = v:
if builtins.isList v then concatStringsSep "," v
else generators.mkValueStringDefault { } v;
mkKeyValue = k: v:
if v == null then ""
else if k == "processChildrenOnly" then ""
else if k == "useTemplate" then ""
else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
in
generators.toINI { inherit mkKeyValue; } cfg.settings;
in
{
# Interface
options.services.sanoid = {
enable = mkEnableOption "Sanoid ZFS snapshotting service";
interval = mkOption {
type = types.str;
default = "hourly";
example = "daily";
description = ''
Run sanoid at this interval. The default is to run hourly.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
datasets = mkOption {
type = types.attrsOf (types.submodule ({ config, options, ... }: {
freeformType = datasetSettingsType;
options = commonOptions // datasetOptions;
config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { });
config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { });
}));
default = { };
description = "Datasets to snapshot.";
};
templates = mkOption {
type = types.attrsOf (types.submodule {
freeformType = datasetSettingsType;
options = commonOptions;
});
default = { };
description = "Templates for datasets.";
};
settings = mkOption {
type = types.attrsOf datasetSettingsType;
description = ''
Free-form settings written directly to the config file. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
for allowed values.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--verbose" "--readonly" "--debug" ];
description = ''
Extra arguments to pass to sanoid. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
for allowed options.
'';
};
};
# Implementation
config = mkIf cfg.enable {
services.sanoid.settings = mkMerge [
(mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
(mapAttrs (d: v: v) cfg.datasets)
];
systemd.services.sanoid = {
description = "Sanoid snapshot service";
serviceConfig = {
ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
ExecStart = lib.escapeShellArgs ([
"${pkgs.sanoid}/bin/sanoid"
"--cron"
"--configdir"
(pkgs.writeTextDir "sanoid.conf" configFile)
] ++ cfg.extraArgs);
User = "sanoid";
Group = "sanoid";
DynamicUser = true;
RuntimeDirectory = "sanoid";
CacheDirectory = "sanoid";
};
# Prevents missing snapshots during DST changes
environment.TZ = "UTC";
after = [ "zfs.target" ];
startAt = cfg.interval;
};
};
meta.maintainers = with maintainers; [ lopsided98 ];
}

View file

@ -0,0 +1,421 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.syncoid;
# Extract local dasaset names (so no datasets containing "@")
localDatasetName = d: optionals (d != null) (
let m = builtins.match "([^/@]+[^@]*)" d; in
optionals (m != null) m
);
# Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
escapeUnitName = name:
lib.concatMapStrings (s: if lib.isList s then "-" else s)
(builtins.split "[^a-zA-Z0-9_.\\-]+" name);
# Function to build "zfs allow" commands for the filesystems we've
# delegated permissions to. It also checks if the target dataset
# exists before delegating permissions, if it doesn't exist we
# delegate it to the parent dataset. This should solve the case of
# provisoning new datasets.
buildAllowCommand = permissions: dataset: (
"-+${pkgs.writeShellScript "zfs-allow-${dataset}" ''
# Here we explicitly use the booted system to guarantee the stable API needed by ZFS
# Run a ZFS list on the dataset to check if it exists
if ${lib.escapeShellArgs [
"/run/booted-system/sw/bin/zfs"
"list"
dataset
]} 2> /dev/null; then
${lib.escapeShellArgs [
"/run/booted-system/sw/bin/zfs"
"allow"
cfg.user
(concatStringsSep "," permissions)
dataset
]}
else
${lib.escapeShellArgs [
"/run/booted-system/sw/bin/zfs"
"allow"
cfg.user
(concatStringsSep "," permissions)
# Remove the last part of the path
(builtins.dirOf dataset)
]}
fi
''}"
);
# Function to build "zfs unallow" commands for the filesystems we've
# delegated permissions to. Here we unallow both the target but also
# on the parent dataset because at this stage we have no way of
# knowing if the allow command did execute on the parent dataset or
# not in the pre-hook. We can't run the same if in the post hook
# since the dataset should have been created at this point.
buildUnallowCommand = permissions: dataset: (
"-+${pkgs.writeShellScript "zfs-unallow-${dataset}" ''
# Here we explicitly use the booted system to guarantee the stable API needed by ZFS
${lib.escapeShellArgs [
"/run/booted-system/sw/bin/zfs"
"unallow"
cfg.user
(concatStringsSep "," permissions)
dataset
]}
${lib.escapeShellArgs [
"/run/booted-system/sw/bin/zfs"
"unallow"
cfg.user
(concatStringsSep "," permissions)
# Remove the last part of the path
(builtins.dirOf dataset)
]}
''}"
);
in
{
# Interface
options.services.syncoid = {
enable = mkEnableOption "Syncoid ZFS synchronization service";
interval = mkOption {
type = types.str;
default = "hourly";
example = "*-*-* *:15:00";
description = ''
Run syncoid at this interval. The default is to run hourly.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
user = mkOption {
type = types.str;
default = "syncoid";
example = "backup";
description = ''
The user for the service. ZFS privilege delegation will be
automatically configured for any local pools used by syncoid if this
option is set to a user other than root. The user will be given the
"hold" and "send" privileges on any pool that has datasets being sent
and the "create", "mount", "receive", and "rollback" privileges on
any pool that has datasets being received.
'';
};
group = mkOption {
type = types.str;
default = "syncoid";
example = "backup";
description = "The group for the service.";
};
sshKey = mkOption {
type = types.nullOr types.path;
# Prevent key from being copied to store
apply = mapNullable toString;
default = null;
description = ''
SSH private key file to use to login to the remote system. Can be
overridden in individual commands.
'';
};
localSourceAllow = mkOption {
type = types.listOf types.str;
# Permissions snapshot and destroy are in case --no-sync-snap is not used
default = [ "bookmark" "hold" "send" "snapshot" "destroy" ];
description = ''
Permissions granted for the <option>services.syncoid.user</option> user
for local source datasets. See
<link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
for available permissions.
'';
};
localTargetAllow = mkOption {
type = types.listOf types.str;
default = [ "change-key" "compression" "create" "mount" "mountpoint" "receive" "rollback" ];
example = [ "create" "mount" "receive" "rollback" ];
description = ''
Permissions granted for the <option>services.syncoid.user</option> user
for local target datasets. See
<link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
for available permissions.
Make sure to include the <literal>change-key</literal> permission if you send raw encrypted datasets,
the <literal>compression</literal> permission if you send raw compressed datasets, and so on.
For remote target datasets you'll have to set your remote user permissions by yourself.
'';
};
commonArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--no-sync-snap" ];
description = ''
Arguments to add to every syncoid command, unless disabled for that
command. See
<link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
for available options.
'';
};
service = mkOption {
type = types.attrs;
default = { };
description = ''
Systemd configuration common to all syncoid services.
'';
};
commands = mkOption {
type = types.attrsOf (types.submodule ({ name, ... }: {
options = {
source = mkOption {
type = types.str;
example = "pool/dataset";
description = ''
Source ZFS dataset. Can be either local or remote. Defaults to
the attribute name.
'';
};
target = mkOption {
type = types.str;
example = "user@server:pool/dataset";
description = ''
Target ZFS dataset. Can be either local
(<replaceable>pool/dataset</replaceable>) or remote
(<replaceable>user@server:pool/dataset</replaceable>).
'';
};
recursive = mkEnableOption ''the transfer of child datasets'';
sshKey = mkOption {
type = types.nullOr types.path;
# Prevent key from being copied to store
apply = mapNullable toString;
description = ''
SSH private key file to use to login to the remote system.
Defaults to <option>services.syncoid.sshKey</option> option.
'';
};
localSourceAllow = mkOption {
type = types.listOf types.str;
description = ''
Permissions granted for the <option>services.syncoid.user</option> user
for local source datasets. See
<link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
for available permissions.
Defaults to <option>services.syncoid.localSourceAllow</option> option.
'';
};
localTargetAllow = mkOption {
type = types.listOf types.str;
description = ''
Permissions granted for the <option>services.syncoid.user</option> user
for local target datasets. See
<link xlink:href="https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html"/>
for available permissions.
Make sure to include the <literal>change-key</literal> permission if you send raw encrypted datasets,
the <literal>compression</literal> permission if you send raw compressed datasets, and so on.
For remote target datasets you'll have to set your remote user permissions by yourself.
'';
};
sendOptions = mkOption {
type = types.separatedString " ";
default = "";
example = "Lc e";
description = ''
Advanced options to pass to zfs send. Options are specified
without their leading dashes and separated by spaces.
'';
};
recvOptions = mkOption {
type = types.separatedString " ";
default = "";
example = "ux recordsize o compression=lz4";
description = ''
Advanced options to pass to zfs recv. Options are specified
without their leading dashes and separated by spaces.
'';
};
useCommonArgs = mkOption {
type = types.bool;
default = true;
description = ''
Whether to add the configured common arguments to this command.
'';
};
service = mkOption {
type = types.attrs;
default = { };
description = ''
Systemd configuration specific to this syncoid service.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--sshport 2222" ];
description = "Extra syncoid arguments for this command.";
};
};
config = {
source = mkDefault name;
sshKey = mkDefault cfg.sshKey;
localSourceAllow = mkDefault cfg.localSourceAllow;
localTargetAllow = mkDefault cfg.localTargetAllow;
};
}));
default = { };
example = literalExpression ''
{
"pool/test".target = "root@target:pool/test";
}
'';
description = "Syncoid commands to run.";
};
};
# Implementation
config = mkIf cfg.enable {
users = {
users = mkIf (cfg.user == "syncoid") {
syncoid = {
group = cfg.group;
isSystemUser = true;
# For syncoid to be able to create /var/lib/syncoid/.ssh/
# and to use custom ssh_config or known_hosts.
home = "/var/lib/syncoid";
createHome = false;
};
};
groups = mkIf (cfg.group == "syncoid") {
syncoid = { };
};
};
systemd.services = mapAttrs'
(name: c:
nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [
{
description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
after = [ "zfs.target" ];
startAt = cfg.interval;
# syncoid may need zpool to get feature@extensible_dataset
path = [ "/run/booted-system/sw/bin/" ];
serviceConfig = {
ExecStartPre =
(map (buildAllowCommand c.localSourceAllow) (localDatasetName c.source)) ++
(map (buildAllowCommand c.localTargetAllow) (localDatasetName c.target));
ExecStopPost =
(map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source)) ++
(map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ]
++ optionals c.useCommonArgs cfg.commonArgs
++ optional c.recursive "-r"
++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
++ c.extraArgs
++ [
"--sendoptions"
c.sendOptions
"--recvoptions"
c.recvOptions
"--no-privilege-elevation"
c.source
c.target
]);
User = cfg.user;
Group = cfg.group;
StateDirectory = [ "syncoid" ];
StateDirectoryMode = "700";
# Prevent SSH control sockets of different syncoid services from interfering
PrivateTmp = true;
# Permissive access to /proc because syncoid
# calls ps(1) to detect ongoing `zfs receive`.
ProcSubset = "all";
ProtectProc = "default";
# The following options are only for optimizing:
# systemd-analyze security | grep syncoid-'*'
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = [ "/dev/zfs" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateNetwork = mkDefault false;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RootDirectory = "/run/syncoid/${escapeUnitName name}";
RootDirectoryStartOnly = true;
BindPaths = [ "/dev/zfs" ];
BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ];
# Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
MountAPIVFS = true;
# Create RootDirectory= in the host's mount namespace.
RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
RuntimeDirectoryMode = "700";
SystemCallFilter = [
"@system-service"
# Groups in @system-service which do not contain a syscall listed by:
# perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
# awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
# systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
"~@aio"
"~@chown"
"~@keyring"
"~@memlock"
"~@privileged"
"~@resources"
"~@setuid"
"~@timer"
];
SystemCallArchitectures = "native";
# This is for BindPaths= and BindReadOnlyPaths=
# to allow traversal of directories they create in RootDirectory=.
UMask = "0066";
};
}
cfg.service
c.service
]))
cfg.commands;
};
meta.maintainers = with maintainers; [ julm lopsided98 ];
}

View file

@ -0,0 +1,408 @@
{ config, lib, options, pkgs, utils, ... }:
with lib;
let
gcfg = config.services.tarsnap;
opt = options.services.tarsnap;
configFile = name: cfg: ''
keyfile ${cfg.keyfile}
${optionalString (cfg.cachedir != null) "cachedir ${cfg.cachedir}"}
${optionalString cfg.nodump "nodump"}
${optionalString cfg.printStats "print-stats"}
${optionalString cfg.printStats "humanize-numbers"}
${optionalString (cfg.checkpointBytes != null) ("checkpoint-bytes "+cfg.checkpointBytes)}
${optionalString cfg.aggressiveNetworking "aggressive-networking"}
${concatStringsSep "\n" (map (v: "exclude ${v}") cfg.excludes)}
${concatStringsSep "\n" (map (v: "include ${v}") cfg.includes)}
${optionalString cfg.lowmem "lowmem"}
${optionalString cfg.verylowmem "verylowmem"}
${optionalString (cfg.maxbw != null) "maxbw ${toString cfg.maxbw}"}
${optionalString (cfg.maxbwRateUp != null) "maxbw-rate-up ${toString cfg.maxbwRateUp}"}
${optionalString (cfg.maxbwRateDown != null) "maxbw-rate-down ${toString cfg.maxbwRateDown}"}
'';
in
{
imports = [
(mkRemovedOptionModule [ "services" "tarsnap" "cachedir" ] "Use services.tarsnap.archives.<name>.cachedir")
];
options = {
services.tarsnap = {
enable = mkEnableOption "periodic tarsnap backups";
keyfile = mkOption {
type = types.str;
default = "/root/tarsnap.key";
description = ''
The keyfile which associates this machine with your tarsnap
account.
Create the keyfile with <command>tarsnap-keygen</command>.
Note that each individual archive (specified below) may also have its
own individual keyfile specified. Tarsnap does not allow multiple
concurrent backups with the same cache directory and key (starting a
new backup will cause another one to fail). If you have multiple
archives specified, you should either spread out your backups to be
far apart, or specify a separate key for each archive. By default
every archive defaults to using
<literal>"/root/tarsnap.key"</literal>.
It's recommended for backups that you generate a key for every archive
using <literal>tarsnap-keygen(1)</literal>, and then generate a
write-only tarsnap key using <literal>tarsnap-keymgmt(1)</literal>,
and keep your master key(s) for a particular machine off-site.
The keyfile name should be given as a string and not a path, to
avoid the key being copied into the Nix store.
'';
};
archives = mkOption {
type = types.attrsOf (types.submodule ({ config, options, ... }:
{
options = {
keyfile = mkOption {
type = types.str;
default = gcfg.keyfile;
defaultText = literalExpression "config.${opt.keyfile}";
description = ''
Set a specific keyfile for this archive. This defaults to
<literal>"/root/tarsnap.key"</literal> if left unspecified.
Use this option if you want to run multiple backups
concurrently - each archive must have a unique key. You can
generate a write-only key derived from your master key (which
is recommended) using <literal>tarsnap-keymgmt(1)</literal>.
Note: every archive must have an individual master key. You
must generate multiple keys with
<literal>tarsnap-keygen(1)</literal>, and then generate write
only keys from those.
The keyfile name should be given as a string and not a path, to
avoid the key being copied into the Nix store.
'';
};
cachedir = mkOption {
type = types.nullOr types.path;
default = "/var/cache/tarsnap/${utils.escapeSystemdPath config.keyfile}";
defaultText = literalExpression ''
"/var/cache/tarsnap/''${utils.escapeSystemdPath config.${options.keyfile}}"
'';
description = ''
The cache allows tarsnap to identify previously stored data
blocks, reducing archival time and bandwidth usage.
Should the cache become desynchronized or corrupted, tarsnap
will refuse to run until you manually rebuild the cache with
<command>tarsnap --fsck</command>.
Set to <literal>null</literal> to disable caching.
'';
};
nodump = mkOption {
type = types.bool;
default = true;
description = ''
Exclude files with the <literal>nodump</literal> flag.
'';
};
printStats = mkOption {
type = types.bool;
default = true;
description = ''
Print global archive statistics upon completion.
The output is available via
<command>systemctl status tarsnap-archive-name</command>.
'';
};
checkpointBytes = mkOption {
type = types.nullOr types.str;
default = "1GB";
description = ''
Create a checkpoint every <literal>checkpointBytes</literal>
of uploaded data (optionally specified using an SI prefix).
1GB is the minimum value. A higher value is recommended,
as checkpointing is expensive.
Set to <literal>null</literal> to disable checkpointing.
'';
};
period = mkOption {
type = types.str;
default = "01:15";
example = "hourly";
description = ''
Create archive at this interval.
The format is described in
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
aggressiveNetworking = mkOption {
type = types.bool;
default = false;
description = ''
Upload data over multiple TCP connections, potentially
increasing tarsnap's bandwidth utilisation at the cost
of slowing down all other network traffic. Not
recommended unless TCP congestion is the dominant
limiting factor.
'';
};
directories = mkOption {
type = types.listOf types.path;
default = [];
description = "List of filesystem paths to archive.";
};
excludes = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Exclude files and directories matching these patterns.
'';
};
includes = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Include only files and directories matching these
patterns (the empty list includes everything).
Exclusions have precedence over inclusions.
'';
};
lowmem = mkOption {
type = types.bool;
default = false;
description = ''
Reduce memory consumption by not caching small files.
Possibly beneficial if the average file size is smaller
than 1 MB and the number of files is lower than the
total amount of RAM in KB.
'';
};
verylowmem = mkOption {
type = types.bool;
default = false;
description = ''
Reduce memory consumption by a factor of 2 beyond what
<literal>lowmem</literal> does, at the cost of significantly
slowing down the archiving process.
'';
};
maxbw = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
Abort archival if upstream bandwidth usage in bytes
exceeds this threshold.
'';
};
maxbwRateUp = mkOption {
type = types.nullOr types.int;
default = null;
example = literalExpression "25 * 1000";
description = ''
Upload bandwidth rate limit in bytes.
'';
};
maxbwRateDown = mkOption {
type = types.nullOr types.int;
default = null;
example = literalExpression "50 * 1000";
description = ''
Download bandwidth rate limit in bytes.
'';
};
verbose = mkOption {
type = types.bool;
default = false;
description = ''
Whether to produce verbose logging output.
'';
};
explicitSymlinks = mkOption {
type = types.bool;
default = false;
description = ''
Whether to follow symlinks specified as archives.
'';
};
followSymlinks = mkOption {
type = types.bool;
default = false;
description = ''
Whether to follow all symlinks in archive trees.
'';
};
};
}
));
default = {};
example = literalExpression ''
{
nixos =
{ directories = [ "/home" "/root/ssl" ];
};
gamedata =
{ directories = [ "/var/lib/minecraft" ];
period = "*:30";
};
}
'';
description = ''
Tarsnap archive configurations. Each attribute names an archive
to be created at a given time interval, according to the options
associated with it. When uploading to the tarsnap server,
archive names are suffixed by a 1 second resolution timestamp,
with the format <literal>%Y%m%d%H%M%S</literal>.
For each member of the set is created a timer which triggers the
instanced <literal>tarsnap-archive-name</literal> service unit. You may use
<command>systemctl start tarsnap-archive-name</command> to
manually trigger creation of <literal>archive-name</literal> at
any time.
'';
};
};
};
config = mkIf gcfg.enable {
assertions =
(mapAttrsToList (name: cfg:
{ assertion = cfg.directories != [];
message = "Must specify paths for tarsnap to back up";
}) gcfg.archives) ++
(mapAttrsToList (name: cfg:
{ assertion = !(cfg.lowmem && cfg.verylowmem);
message = "You cannot set both lowmem and verylowmem";
}) gcfg.archives);
systemd.services =
(mapAttrs' (name: cfg: nameValuePair "tarsnap-${name}" {
description = "Tarsnap archive '${name}'";
requires = [ "network-online.target" ];
after = [ "network-online.target" ];
path = with pkgs; [ iputils tarsnap util-linux ];
# In order for the persistent tarsnap timer to work reliably, we have to
# make sure that the tarsnap server is reachable after systemd starts up
# the service - therefore we sleep in a loop until we can ping the
# endpoint.
preStart = ''
while ! ping -4 -q -c 1 v1-0-0-server.tarsnap.com &> /dev/null; do sleep 3; done
'';
script = let
tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
run = ''${tarsnap} -c -f "${name}-$(date +"%Y%m%d%H%M%S")" \
${optionalString cfg.verbose "-v"} \
${optionalString cfg.explicitSymlinks "-H"} \
${optionalString cfg.followSymlinks "-L"} \
${concatStringsSep " " cfg.directories}'';
cachedir = escapeShellArg cfg.cachedir;
in if (cfg.cachedir != null) then ''
mkdir -p ${cachedir}
chmod 0700 ${cachedir}
( flock 9
if [ ! -e ${cachedir}/firstrun ]; then
( flock 10
flock -u 9
${tarsnap} --fsck
flock 9
) 10>${cachedir}/firstrun
fi
) 9>${cachedir}/lockf
exec flock ${cachedir}/firstrun ${run}
'' else "exec ${run}";
serviceConfig = {
Type = "oneshot";
IOSchedulingClass = "idle";
NoNewPrivileges = "true";
CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
PermissionsStartOnly = "true";
};
}) gcfg.archives) //
(mapAttrs' (name: cfg: nameValuePair "tarsnap-restore-${name}"{
description = "Tarsnap restore '${name}'";
requires = [ "network-online.target" ];
path = with pkgs; [ iputils tarsnap util-linux ];
script = let
tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
lastArchive = "$(${tarsnap} --list-archives | sort | tail -1)";
run = ''${tarsnap} -x -f "${lastArchive}" ${optionalString cfg.verbose "-v"}'';
cachedir = escapeShellArg cfg.cachedir;
in if (cfg.cachedir != null) then ''
mkdir -p ${cachedir}
chmod 0700 ${cachedir}
( flock 9
if [ ! -e ${cachedir}/firstrun ]; then
( flock 10
flock -u 9
${tarsnap} --fsck
flock 9
) 10>${cachedir}/firstrun
fi
) 9>${cachedir}/lockf
exec flock ${cachedir}/firstrun ${run}
'' else "exec ${run}";
serviceConfig = {
Type = "oneshot";
IOSchedulingClass = "idle";
NoNewPrivileges = "true";
CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ];
PermissionsStartOnly = "true";
};
}) gcfg.archives);
# Note: the timer must be Persistent=true, so that systemd will start it even
# if e.g. your laptop was asleep while the latest interval occurred.
systemd.timers = mapAttrs' (name: cfg: nameValuePair "tarsnap-${name}"
{ timerConfig.OnCalendar = cfg.period;
timerConfig.Persistent = "true";
wantedBy = [ "timers.target" ];
}) gcfg.archives;
environment.etc =
mapAttrs' (name: cfg: nameValuePair "tarsnap/${name}.conf"
{ text = configFile name cfg;
}) gcfg.archives;
environment.systemPackages = [ pkgs.tarsnap ];
};
}

View file

@ -0,0 +1,125 @@
{ config, lib, ... }:
let
inherit (lib.attrsets) hasAttr;
inherit (lib.modules) mkDefault mkIf;
inherit (lib.options) mkEnableOption mkOption;
inherit (lib.types) nonEmptyStr nullOr;
options.services.tsmBackup = {
enable = mkEnableOption ''
automatic backups with the
IBM Spectrum Protect (Tivoli Storage Manager, TSM) client.
This also enables
<option>programs.tsmClient.enable</option>
'';
command = mkOption {
type = nonEmptyStr;
default = "backup";
example = "incr";
description = ''
The actual command passed to the
<literal>dsmc</literal> executable to start the backup.
'';
};
servername = mkOption {
type = nonEmptyStr;
example = "mainTsmServer";
description = ''
Create a systemd system service
<literal>tsm-backup.service</literal> that starts
a backup based on the given servername's stanza.
Note that this server's
<option>passwdDir</option> will default to
<filename>/var/lib/tsm-backup/password</filename>
(but may be overridden);
also, the service will use
<filename>/var/lib/tsm-backup</filename> as
<literal>HOME</literal> when calling
<literal>dsmc</literal>.
'';
};
autoTime = mkOption {
type = nullOr nonEmptyStr;
default = null;
example = "12:00";
description = ''
The backup service will be invoked
automatically at the given date/time,
which must be in the format described in
<citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
The default <literal>null</literal>
disables automatic backups.
'';
};
};
cfg = config.services.tsmBackup;
cfgPrg = config.programs.tsmClient;
assertions = [
{
assertion = hasAttr cfg.servername cfgPrg.servers;
message = "TSM service servername not found in list of servers";
}
{
assertion = cfgPrg.servers.${cfg.servername}.genPasswd;
message = "TSM service requires automatic password generation";
}
];
in
{
inherit options;
config = mkIf cfg.enable {
inherit assertions;
programs.tsmClient.enable = true;
programs.tsmClient.servers.${cfg.servername}.passwdDir =
mkDefault "/var/lib/tsm-backup/password";
systemd.services.tsm-backup = {
description = "IBM Spectrum Protect (Tivoli Storage Manager) Backup";
# DSM_LOG needs a trailing slash to have it treated as a directory.
# `/var/log` would be littered with TSM log files otherwise.
environment.DSM_LOG = "/var/log/tsm-backup/";
# TSM needs a HOME dir to store certificates.
environment.HOME = "/var/lib/tsm-backup";
serviceConfig = {
# for exit status description see
# https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=clients-client-return-codes
SuccessExitStatus = "4 8";
# The `-se` option must come after the command.
# The `-optfile` option suppresses a `dsm.opt`-not-found warning.
ExecStart =
"${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null";
LogsDirectory = "tsm-backup";
StateDirectory = "tsm-backup";
StateDirectoryMode = "0750";
# systemd sandboxing
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
#PrivateTmp = true; # would break backup of {/var,}/tmp
#PrivateUsers = true; # would block backup of /home/*
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = "read-only";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "noaccess";
ProtectSystem = "strict";
RestrictNamespaces = true;
RestrictSUIDSGID = true;
};
startAt = mkIf (cfg.autoTime!=null) cfg.autoTime;
};
};
meta.maintainers = [ lib.maintainers.yarny ];
}

View file

@ -0,0 +1,90 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.services.zfs.autoReplication;
recursive = optionalString cfg.recursive " --recursive";
followDelete = optionalString cfg.followDelete " --follow-delete";
in {
options = {
services.zfs.autoReplication = {
enable = mkEnableOption "ZFS snapshot replication.";
followDelete = mkOption {
description = "Remove remote snapshots that don't have a local correspondant.";
default = true;
type = types.bool;
};
host = mkOption {
description = "Remote host where snapshots should be sent. <literal>lz4</literal> is expected to be installed on this host.";
example = "example.com";
type = types.str;
};
identityFilePath = mkOption {
description = "Path to SSH key used to login to host.";
example = "/home/username/.ssh/id_rsa";
type = types.path;
};
localFilesystem = mkOption {
description = "Local ZFS fileystem from which snapshots should be sent. Defaults to the attribute name.";
example = "pool/file/path";
type = types.str;
};
remoteFilesystem = mkOption {
description = "Remote ZFS filesystem where snapshots should be sent.";
example = "pool/file/path";
type = types.str;
};
recursive = mkOption {
description = "Recursively discover snapshots to send.";
default = true;
type = types.bool;
};
username = mkOption {
description = "Username used by SSH to login to remote host.";
example = "username";
type = types.str;
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
pkgs.lz4
];
systemd.services.zfs-replication = {
after = [
"zfs-snapshot-daily.service"
"zfs-snapshot-frequent.service"
"zfs-snapshot-hourly.service"
"zfs-snapshot-monthly.service"
"zfs-snapshot-weekly.service"
];
description = "ZFS Snapshot Replication";
documentation = [
"https://github.com/alunduil/zfs-replicate"
];
restartIfChanged = false;
serviceConfig.ExecStart = "${pkgs.zfs-replicate}/bin/zfs-replicate${recursive} -l ${escapeShellArg cfg.username} -i ${escapeShellArg cfg.identityFilePath}${followDelete} ${escapeShellArg cfg.host} ${escapeShellArg cfg.remoteFilesystem} ${escapeShellArg cfg.localFilesystem}";
wantedBy = [
"zfs-snapshot-daily.service"
"zfs-snapshot-frequent.service"
"zfs-snapshot-hourly.service"
"zfs-snapshot-monthly.service"
"zfs-snapshot-weekly.service"
];
};
};
meta = {
maintainers = with lib.maintainers; [ alunduil ];
};
}

View file

@ -0,0 +1,469 @@
{ config, lib, pkgs, ... }:
with lib;
with types;
let
planDescription = ''
The znapzend backup plan to use for the source.
The plan specifies how often to backup and for how long to keep the
backups. It consists of a series of retention periodes to interval
associations:
<literal>
retA=>intA,retB=>intB,...
</literal>
Both intervals and retention periods are expressed in standard units
of time or multiples of them. You can use both the full name or a
shortcut according to the following listing:
<literal>
second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y
</literal>
See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info.
'';
planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m";
# A type for a string of the form number{b|k|M|G}
mbufferSizeType = str // {
check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x);
description = "string of the form number{b|k|M|G}";
};
enabledFeatures = concatLists (mapAttrsToList (name: enabled: optional enabled name) cfg.features);
# Type for a string that must contain certain other strings (the list parameter).
# Note that these would need regex escaping.
stringContainingStrings = list: let
matching = s: map (str: builtins.match ".*${str}.*" s) list;
in str // {
check = x: str.check x && all isList (matching x);
description = "string containing all of the characters ${concatStringsSep ", " list}";
};
timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ];
destType = srcConfig: submodule ({ name, ... }: {
options = {
label = mkOption {
type = str;
description = "Label for this destination. Defaults to the attribute name.";
};
plan = mkOption {
type = str;
description = planDescription;
example = planExample;
};
dataset = mkOption {
type = str;
description = "Dataset name to send snapshots to.";
example = "tank/main";
};
host = mkOption {
type = nullOr str;
description = ''
Host to use for the destination dataset. Can be prefixed with
<literal>user@</literal> to specify the ssh user.
'';
default = null;
example = "john@example.com";
};
presend = mkOption {
type = nullOr str;
description = ''
Command to run before sending the snapshot to the destination.
Intended to run a remote script via <command>ssh</command> on the
destination, e.g. to bring up a backup disk or server or to put a
zpool online/offline. See also <option>postsend</option>.
'';
default = null;
example = "ssh root@bserv zpool import -Nf tank";
};
postsend = mkOption {
type = nullOr str;
description = ''
Command to run after sending the snapshot to the destination.
Intended to run a remote script via <command>ssh</command> on the
destination, e.g. to bring up a backup disk or server or to put a
zpool online/offline. See also <option>presend</option>.
'';
default = null;
example = "ssh root@bserv zpool export tank";
};
};
config = {
label = mkDefault name;
plan = mkDefault srcConfig.plan;
};
});
srcType = submodule ({ name, config, ... }: {
options = {
enable = mkOption {
type = bool;
description = "Whether to enable this source.";
default = true;
};
recursive = mkOption {
type = bool;
description = "Whether to do recursive snapshots.";
default = false;
};
mbuffer = {
enable = mkOption {
type = bool;
description = "Whether to use <command>mbuffer</command>.";
default = false;
};
port = mkOption {
type = nullOr ints.u16;
description = ''
Port to use for <command>mbuffer</command>.
If this is null, it will run <command>mbuffer</command> through
ssh.
If this is not null, it will run <command>mbuffer</command>
directly through TCP, which is not encrypted but faster. In that
case the given port needs to be open on the destination host.
'';
default = null;
};
size = mkOption {
type = mbufferSizeType;
description = ''
The size for <command>mbuffer</command>.
Supports the units b, k, M, G.
'';
default = "1G";
example = "128M";
};
};
presnap = mkOption {
type = nullOr str;
description = ''
Command to run before snapshots are taken on the source dataset,
e.g. for database locking/flushing. See also
<option>postsnap</option>.
'';
default = null;
example = literalExpression ''
'''''${pkgs.mariadb}/bin/mysql -e "set autocommit=0;flush tables with read lock;\\! ''${pkgs.coreutils}/bin/sleep 600" & ''${pkgs.coreutils}/bin/echo $! > /tmp/mariadblock.pid ; sleep 10'''
'';
};
postsnap = mkOption {
type = nullOr str;
description = ''
Command to run after snapshots are taken on the source dataset,
e.g. for database unlocking. See also <option>presnap</option>.
'';
default = null;
example = literalExpression ''
"''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid"
'';
};
timestampFormat = mkOption {
type = timestampType;
description = ''
The timestamp format to use for constructing snapshot names.
The syntax is <literal>strftime</literal>-like. The string must
consist of the mandatory <literal>%Y %m %d %H %M %S</literal>.
Optionally <literal>- _ . :</literal> characters as well as any
alphanumeric character are allowed. If suffixed by a
<literal>Z</literal>, times will be in UTC.
'';
default = "%Y-%m-%d-%H%M%S";
example = "znapzend-%m.%d.%Y-%H%M%SZ";
};
sendDelay = mkOption {
type = int;
description = ''
Specify delay (in seconds) before sending snaps to the destination.
May be useful if you want to control sending time.
'';
default = 0;
example = 60;
};
plan = mkOption {
type = str;
description = planDescription;
example = planExample;
};
dataset = mkOption {
type = str;
description = "The dataset to use for this source.";
example = "tank/home";
};
destinations = mkOption {
type = attrsOf (destType config);
description = "Additional destinations.";
default = {};
example = literalExpression ''
{
local = {
dataset = "btank/backup";
presend = "zpool import -N btank";
postsend = "zpool export btank";
};
remote = {
host = "john@example.com";
dataset = "tank/john";
};
};
'';
};
};
config = {
dataset = mkDefault name;
};
});
### Generating the configuration from here
cfg = config.services.znapzend;
onOff = b: if b then "on" else "off";
nullOff = b: if b == null then "off" else toString b;
stripSlashes = replaceStrings [ "/" ] [ "." ];
attrsToFile = config: concatStringsSep "\n" (builtins.attrValues (
mapAttrs (n: v: "${n}=${v}") config));
mkDestAttrs = dst: with dst;
mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({
"" = optionalString (host != null) "${host}:" + dataset;
_plan = plan;
} // optionalAttrs (presend != null) {
_precmd = presend;
} // optionalAttrs (postsend != null) {
_pstcmd = postsend;
});
mkSrcAttrs = srcCfg: with srcCfg; {
enabled = onOff enable;
# mbuffer is not referenced by its full path to accomodate non-NixOS systems or differing mbuffer versions between source and target
mbuffer = with mbuffer; if enable then "mbuffer"
+ optionalString (port != null) ":${toString port}" else "off";
mbuffer_size = mbuffer.size;
post_znap_cmd = nullOff postsnap;
pre_znap_cmd = nullOff presnap;
recursive = onOff recursive;
src = dataset;
src_plan = plan;
tsformat = timestampFormat;
zend_delay = toString sendDelay;
} // foldr (a: b: a // b) {} (
map mkDestAttrs (builtins.attrValues destinations)
);
files = mapAttrs' (n: srcCfg: let
fileText = attrsToFile (mkSrcAttrs srcCfg);
in {
name = srcCfg.dataset;
value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText;
}) cfg.zetup;
in
{
options = {
services.znapzend = {
enable = mkEnableOption "ZnapZend ZFS backup daemon";
logLevel = mkOption {
default = "debug";
example = "warning";
type = enum ["debug" "info" "warning" "err" "alert"];
description = ''
The log level when logging to file. Any of debug, info, warning, err,
alert. Default in daemonized form is debug.
'';
};
logTo = mkOption {
type = str;
default = "syslog::daemon";
example = "/var/log/znapzend.log";
description = ''
Where to log to (syslog::&lt;facility&gt; or &lt;filepath&gt;).
'';
};
noDestroy = mkOption {
type = bool;
default = false;
description = "Does all changes to the filesystem except destroy.";
};
autoCreation = mkOption {
type = bool;
default = false;
description = "Automatically create the destination dataset if it does not exist.";
};
zetup = mkOption {
type = attrsOf srcType;
description = "Znapzend configuration.";
default = {};
example = literalExpression ''
{
"tank/home" = {
# Make snapshots of tank/home every hour, keep those for 1 day,
# keep every days snapshot for 1 month, etc.
plan = "1d=>1h,1m=>1d,1y=>1m";
recursive = true;
# Send all those snapshots to john@example.com:rtank/john as well
destinations.remote = {
host = "john@example.com";
dataset = "rtank/john";
};
};
};
'';
};
pure = mkOption {
type = bool;
description = ''
Do not persist any stateful znapzend setups. If this option is
enabled, your previously set znapzend setups will be cleared and only
the ones defined with this module will be applied.
'';
default = false;
};
features.oracleMode = mkEnableOption ''
Destroy snapshots one by one instead of using one long argument list.
If source and destination are out of sync for a long time, you may have
so many snapshots to destroy that the argument gets is too long and the
command fails.
'';
features.recvu = mkEnableOption ''
recvu feature which uses <literal>-u</literal> on the receiving end to keep the destination
filesystem unmounted.
'';
features.compressed = mkEnableOption ''
compressed feature which adds the options <literal>-Lce</literal> to
the <command>zfs send</command> command. When this is enabled, make
sure that both the sending and receiving pool have the same relevant
features enabled. Using <literal>-c</literal> will skip unneccessary
decompress-compress stages, <literal>-L</literal> is for large block
support and -e is for embedded data support. see
<citerefentry><refentrytitle>znapzend</refentrytitle><manvolnum>1</manvolnum></citerefentry>
and <citerefentry><refentrytitle>zfs</refentrytitle><manvolnum>8</manvolnum></citerefentry>
for more info.
'';
features.sendRaw = mkEnableOption ''
sendRaw feature which adds the options <literal>-w</literal> to the
<command>zfs send</command> command. For encrypted source datasets this
instructs zfs not to decrypt before sending which results in a remote
backup that can't be read without the encryption key/passphrase, useful
when the remote isn't fully trusted or not physically secure. This
option must be used consistently, raw incrementals cannot be based on
non-raw snapshots and vice versa.
'';
features.skipIntermediates = mkEnableOption ''
Enable the skipIntermediates feature to send a single increment
between latest common snapshot and the newly made one. It may skip
several source snaps if the destination was offline for some time, and
it should skip snapshots not managed by znapzend. Normally for online
destinations, the new snapshot is sent as soon as it is created on the
source, so there are no automatic increments to skip.
'';
features.lowmemRecurse = mkEnableOption ''
use lowmemRecurse on systems where you have too many datasets, so a
recursive listing of attributes to find backup plans exhausts the
memory available to <command>znapzend</command>: instead, go the slower
way to first list all impacted dataset names, and then query their
configs one by one.
'';
features.zfsGetType = mkEnableOption ''
use zfsGetType if your <command>zfs get</command> supports a
<literal>-t</literal> argument for filtering by dataset type at all AND
lists properties for snapshots by default when recursing, so that there
is too much data to process while searching for backup plans.
If these two conditions apply to your system, the time needed for a
<literal>--recursive</literal> search for backup plans can literally
differ by hundreds of times (depending on the amount of snapshots in
that dataset tree... and a decent backup plan will ensure you have a lot
of those), so you would benefit from requesting this feature.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.znapzend ];
systemd.services = {
znapzend = {
description = "ZnapZend - ZFS Backup System";
wantedBy = [ "zfs.target" ];
after = [ "zfs.target" ];
path = with pkgs; [ zfs mbuffer openssh ];
preStart = optionalString cfg.pure ''
echo Resetting znapzend zetups
${pkgs.znapzend}/bin/znapzendzetup list \
| grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \
| xargs -I{} ${pkgs.znapzend}/bin/znapzendzetup delete "{}"
'' + concatStringsSep "\n" (mapAttrsToList (dataset: config: ''
echo Importing znapzend zetup ${config} for dataset ${dataset}
${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config} &
'') files) + ''
wait
'';
serviceConfig = {
# znapzendzetup --import apparently tries to connect to the backup
# host 3 times with a timeout of 30 seconds, leading to a startup
# delay of >90s when the host is down, which is just above the default
# service timeout of 90 seconds. Increase the timeout so it doesn't
# make the service fail in that case.
TimeoutStartSec = 180;
# Needs to have write access to ZFS
User = "root";
ExecStart = let
args = concatStringsSep " " [
"--logto=${cfg.logTo}"
"--loglevel=${cfg.logLevel}"
(optionalString cfg.noDestroy "--nodestroy")
(optionalString cfg.autoCreation "--autoCreation")
(optionalString (enabledFeatures != [])
"--features=${concatStringsSep "," enabledFeatures}")
]; in "${pkgs.znapzend}/bin/znapzend ${args}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "on-failure";
};
};
};
};
meta.maintainers = with maintainers; [ infinisil SlothOfAnarchy ];
}

View file

@ -0,0 +1,57 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.zrepl;
format = pkgs.formats.yaml { };
configFile = format.generate "zrepl.yml" cfg.settings;
in
{
meta.maintainers = with maintainers; [ cole-h ];
options = {
services.zrepl = {
enable = mkEnableOption "zrepl";
settings = mkOption {
default = { };
description = ''
Configuration for zrepl. See <link
xlink:href="https://zrepl.github.io/configuration.html"/>
for more information.
'';
type = types.submodule {
freeformType = format.type;
};
};
};
};
### Implementation ###
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.zrepl ];
# zrepl looks for its config in this location by default. This
# allows the use of e.g. `zrepl signal wakeup <job>` without having
# to specify the storepath of the config.
environment.etc."zrepl/zrepl.yml".source = configFile;
systemd.packages = [ pkgs.zrepl ];
# Note that pkgs.zrepl copies and adapts the upstream systemd unit, and
# the fields defined here only override certain fields from that unit.
systemd.services.zrepl = {
requires = [ "local-fs.target" ];
wantedBy = [ "zfs.target" ];
after = [ "zfs.target" ];
path = [ config.boot.zfs.package ];
restartTriggers = [ configFile ];
serviceConfig = {
Restart = "on-failure";
};
};
};
}

View file

@ -0,0 +1,179 @@
{ config, lib, pkgs, ... }:
with lib;
let
eachGeth = config.services.geth;
gethOpts = { config, lib, name, ...}: {
options = {
enable = lib.mkEnableOption "Go Ethereum Node";
port = mkOption {
type = types.port;
default = 30303;
description = "Port number Go Ethereum will be listening on, both TCP and UDP.";
};
http = {
enable = lib.mkEnableOption "Go Ethereum HTTP API";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Listen address of Go Ethereum HTTP API.";
};
port = mkOption {
type = types.port;
default = 8545;
description = "Port number of Go Ethereum HTTP API.";
};
apis = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "APIs to enable over WebSocket";
example = ["net" "eth"];
};
};
websocket = {
enable = lib.mkEnableOption "Go Ethereum WebSocket API";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Listen address of Go Ethereum WebSocket API.";
};
port = mkOption {
type = types.port;
default = 8546;
description = "Port number of Go Ethereum WebSocket API.";
};
apis = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "APIs to enable over WebSocket";
example = ["net" "eth"];
};
};
metrics = {
enable = lib.mkEnableOption "Go Ethereum prometheus metrics";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Listen address of Go Ethereum metrics service.";
};
port = mkOption {
type = types.port;
default = 6060;
description = "Port number of Go Ethereum metrics service.";
};
};
network = mkOption {
type = types.nullOr (types.enum [ "goerli" "rinkeby" "yolov2" "ropsten" ]);
default = null;
description = "The network to connect to. Mainnet (null) is the default ethereum network.";
};
syncmode = mkOption {
type = types.enum [ "snap" "fast" "full" "light" ];
default = "snap";
description = "Blockchain sync mode.";
};
gcmode = mkOption {
type = types.enum [ "full" "archive" ];
default = "full";
description = "Blockchain garbage collection mode.";
};
maxpeers = mkOption {
type = types.int;
default = 50;
description = "Maximum peers to connect to.";
};
extraArgs = mkOption {
type = types.listOf types.str;
description = "Additional arguments passed to Go Ethereum.";
default = [];
};
package = mkOption {
default = pkgs.go-ethereum.geth;
defaultText = literalExpression "pkgs.go-ethereum.geth";
type = types.package;
description = "Package to use as Go Ethereum node.";
};
};
};
in
{
###### interface
options = {
services.geth = mkOption {
type = types.attrsOf (types.submodule gethOpts);
default = {};
description = "Specification of one or more geth instances.";
};
};
###### implementation
config = mkIf (eachGeth != {}) {
environment.systemPackages = flatten (mapAttrsToList (gethName: cfg: [
cfg.package
]) eachGeth);
systemd.services = mapAttrs' (gethName: cfg: (
nameValuePair "geth-${gethName}" (mkIf cfg.enable {
description = "Go Ethereum node (${gethName})";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
Restart = "always";
StateDirectory = "goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}";
# Hardening measures
PrivateTmp = "true";
ProtectSystem = "full";
NoNewPrivileges = "true";
PrivateDevices = "true";
MemoryDenyWriteExecute = "true";
};
script = ''
${cfg.package}/bin/geth \
--nousb \
--ipcdisable \
${optionalString (cfg.network != null) ''--${cfg.network}''} \
--syncmode ${cfg.syncmode} \
--gcmode ${cfg.gcmode} \
--port ${toString cfg.port} \
--maxpeers ${toString cfg.maxpeers} \
${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \
${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \
${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \
${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \
${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \
${lib.escapeShellArgs cfg.extraArgs} \
--datadir /var/lib/goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}
'';
}))) eachGeth;
};
}

View file

@ -0,0 +1,112 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.corosync;
in
{
# interface
options.services.corosync = {
enable = mkEnableOption "corosync";
package = mkOption {
type = types.package;
default = pkgs.corosync;
defaultText = literalExpression "pkgs.corosync";
description = "Package that should be used for corosync.";
};
clusterName = mkOption {
type = types.str;
default = "nixcluster";
description = "Name of the corosync cluster.";
};
extraOptions = mkOption {
type = with types; listOf str;
default = [];
description = "Additional options with which to start corosync.";
};
nodelist = mkOption {
description = "Corosync nodelist: all cluster members.";
default = [];
type = with types; listOf (submodule {
options = {
nodeid = mkOption {
type = int;
description = "Node ID number";
};
name = mkOption {
type = str;
description = "Node name";
};
ring_addrs = mkOption {
type = listOf str;
description = "List of addresses, one for each ring.";
};
};
});
};
};
# implementation
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
environment.etc."corosync/corosync.conf".text = ''
totem {
version: 2
secauth: on
cluster_name: ${cfg.clusterName}
transport: knet
}
nodelist {
${concatMapStrings ({ nodeid, name, ring_addrs }: ''
node {
nodeid: ${toString nodeid}
name: ${name}
${concatStrings (imap0 (i: addr: ''
ring${toString i}_addr: ${addr}
'') ring_addrs)}
}
'') cfg.nodelist}
}
quorum {
# only corosync_votequorum is supported
provider: corosync_votequorum
wait_for_all: 0
${optionalString (builtins.length cfg.nodelist < 3) ''
two_node: 1
''}
}
logging {
to_syslog: yes
}
'';
environment.etc."corosync/uidgid.d/root".text = ''
# allow pacemaker connection by root
uidgid {
uid: 0
gid: 0
}
'';
systemd.packages = [ cfg.package ];
systemd.services.corosync = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "corosync";
StateDirectoryMode = "0700";
};
};
environment.etc."sysconfig/corosync".text = lib.optionalString (cfg.extraOptions != []) ''
COROSYNC_OPTIONS="${lib.escapeShellArgs cfg.extraOptions}"
'';
};
}

View file

@ -0,0 +1,44 @@
{ cfg, pkgs, lib }:
let
propertyXml = name: value: lib.optionalString (value != null) ''
<property>
<name>${name}</name>
<value>${builtins.toString value}</value>
</property>
'';
siteXml = fileName: properties: pkgs.writeTextDir fileName ''
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- generated by NixOS -->
<configuration>
${builtins.concatStringsSep "\n" (pkgs.lib.mapAttrsToList propertyXml properties)}
</configuration>
'';
cfgLine = name: value: ''
${name}=${builtins.toString value}
'';
cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
# generated by NixOS
${builtins.concatStringsSep "" (pkgs.lib.mapAttrsToList cfgLine properties)}
'';
userFunctions = ''
hadoop_verify_logdir() {
echo Skipping verification of log directory
}
'';
hadoopEnv = ''
export HADOOP_LOG_DIR=/tmp/hadoop/$USER
'';
in
pkgs.runCommand "hadoop-conf" {} (with cfg; ''
mkdir -p $out/
cp ${siteXml "core-site.xml" (coreSite // coreSiteInternal)}/* $out/
cp ${siteXml "hdfs-site.xml" (hdfsSiteDefault // hdfsSite // hdfsSiteInternal)}/* $out/
cp ${siteXml "mapred-site.xml" (mapredSiteDefault // mapredSite)}/* $out/
cp ${siteXml "yarn-site.xml" (yarnSiteDefault // yarnSite // yarnSiteInternal)}/* $out/
cp ${siteXml "httpfs-site.xml" httpfsSite}/* $out/
cp ${cfgFile "container-executor.cfg" containerExecutorCfg}/* $out/
cp ${pkgs.writeTextDir "hadoop-user-functions.sh" userFunctions}/* $out/
cp ${pkgs.writeTextDir "hadoop-env.sh" hadoopEnv}/* $out/
cp ${log4jProperties} $out/log4j.properties
${lib.concatMapStringsSep "\n" (dir: "cp -r ${dir}/* $out/") extraConfDirs}
'')

View file

@ -0,0 +1,223 @@
{ config, lib, options, pkgs, ...}:
let
cfg = config.services.hadoop;
opt = options.services.hadoop;
in
with lib;
{
imports = [ ./yarn.nix ./hdfs.nix ];
options.services.hadoop = {
coreSite = mkOption {
default = {};
type = types.attrsOf types.anything;
example = literalExpression ''
{
"fs.defaultFS" = "hdfs://localhost";
}
'';
description = ''
Hadoop core-site.xml definition
<link xlink:href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/core-default.xml"/>
'';
};
coreSiteInternal = mkOption {
default = {};
type = types.attrsOf types.anything;
internal = true;
description = ''
Internal option to add configs to core-site.xml based on module options
'';
};
hdfsSiteDefault = mkOption {
default = {
"dfs.namenode.rpc-bind-host" = "0.0.0.0";
"dfs.namenode.http-address" = "0.0.0.0:9870";
"dfs.namenode.servicerpc-bind-host" = "0.0.0.0";
"dfs.namenode.http-bind-host" = "0.0.0.0";
};
type = types.attrsOf types.anything;
description = ''
Default options for hdfs-site.xml
'';
};
hdfsSite = mkOption {
default = {};
type = types.attrsOf types.anything;
example = literalExpression ''
{
"dfs.nameservices" = "namenode1";
}
'';
description = ''
Additional options and overrides for hdfs-site.xml
<link xlink:href="https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/hdfs-default.xml"/>
'';
};
hdfsSiteInternal = mkOption {
default = {};
type = types.attrsOf types.anything;
internal = true;
description = ''
Internal option to add configs to hdfs-site.xml based on module options
'';
};
mapredSiteDefault = mkOption {
default = {
"mapreduce.framework.name" = "yarn";
"yarn.app.mapreduce.am.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
"mapreduce.map.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
"mapreduce.reduce.env" = "HADOOP_MAPRED_HOME=${cfg.package}/lib/${cfg.package.untarDir}";
};
defaultText = literalExpression ''
{
"mapreduce.framework.name" = "yarn";
"yarn.app.mapreduce.am.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
"mapreduce.map.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
"mapreduce.reduce.env" = "HADOOP_MAPRED_HOME=''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}";
}
'';
type = types.attrsOf types.anything;
description = ''
Default options for mapred-site.xml
'';
};
mapredSite = mkOption {
default = {};
type = types.attrsOf types.anything;
example = literalExpression ''
{
"mapreduce.map.java.opts" = "-Xmx900m -XX:+UseParallelGC";
}
'';
description = ''
Additional options and overrides for mapred-site.xml
<link xlink:href="https://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml"/>
'';
};
yarnSiteDefault = mkOption {
default = {
"yarn.nodemanager.admin-env" = "PATH=$PATH";
"yarn.nodemanager.aux-services" = "mapreduce_shuffle";
"yarn.nodemanager.aux-services.mapreduce_shuffle.class" = "org.apache.hadoop.mapred.ShuffleHandler";
"yarn.nodemanager.bind-host" = "0.0.0.0";
"yarn.nodemanager.container-executor.class" = "org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor";
"yarn.nodemanager.env-whitelist" = "JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_HOME,LANG,TZ";
"yarn.nodemanager.linux-container-executor.group" = "hadoop";
"yarn.nodemanager.linux-container-executor.path" = "/run/wrappers/yarn-nodemanager/bin/container-executor";
"yarn.nodemanager.log-dirs" = "/var/log/hadoop/yarn/nodemanager";
"yarn.resourcemanager.bind-host" = "0.0.0.0";
"yarn.resourcemanager.scheduler.class" = "org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler";
};
type = types.attrsOf types.anything;
description = ''
Default options for yarn-site.xml
'';
};
yarnSite = mkOption {
default = {};
type = types.attrsOf types.anything;
example = literalExpression ''
{
"yarn.resourcemanager.hostname" = "''${config.networking.hostName}";
}
'';
description = ''
Additional options and overrides for yarn-site.xml
<link xlink:href="https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-common/yarn-default.xml"/>
'';
};
yarnSiteInternal = mkOption {
default = {};
type = types.attrsOf types.anything;
internal = true;
description = ''
Internal option to add configs to yarn-site.xml based on module options
'';
};
httpfsSite = mkOption {
default = { };
type = types.attrsOf types.anything;
example = literalExpression ''
{
"hadoop.http.max.threads" = 500;
}
'';
description = ''
Hadoop httpfs-site.xml definition
<link xlink:href="https://hadoop.apache.org/docs/current/hadoop-hdfs-httpfs/httpfs-default.html"/>
'';
};
log4jProperties = mkOption {
default = "${cfg.package}/lib/${cfg.package.untarDir}/etc/hadoop/log4j.properties";
defaultText = literalExpression ''
"''${config.${opt.package}}/lib/''${config.${opt.package}.untarDir}/etc/hadoop/log4j.properties"
'';
type = types.path;
example = literalExpression ''
"''${pkgs.hadoop}/lib/''${pkgs.hadoop.untarDir}/etc/hadoop/log4j.properties";
'';
description = "log4j.properties file added to HADOOP_CONF_DIR";
};
containerExecutorCfg = mkOption {
default = {
# must be the same as yarn.nodemanager.linux-container-executor.group in yarnSite
"yarn.nodemanager.linux-container-executor.group"="hadoop";
"min.user.id"=1000;
"feature.terminal.enabled"=1;
"feature.mount-cgroup.enabled" = 1;
};
type = types.attrsOf types.anything;
example = literalExpression ''
options.services.hadoop.containerExecutorCfg.default // {
"feature.terminal.enabled" = 0;
}
'';
description = ''
Yarn container-executor.cfg definition
<link xlink:href="https://hadoop.apache.org/docs/r2.7.2/hadoop-yarn/hadoop-yarn-site/SecureContainer.html"/>
'';
};
extraConfDirs = mkOption {
default = [];
type = types.listOf types.path;
example = literalExpression ''
[
./extraHDFSConfs
./extraYARNConfs
]
'';
description = "Directories containing additional config files to be added to HADOOP_CONF_DIR";
};
gatewayRole.enable = mkEnableOption "gateway role for deploying hadoop configs";
package = mkOption {
type = types.package;
default = pkgs.hadoop;
defaultText = literalExpression "pkgs.hadoop";
description = "";
};
};
config = mkIf cfg.gatewayRole.enable {
users.groups.hadoop = {
gid = config.ids.gids.hadoop;
};
environment = {
systemPackages = [ cfg.package ];
etc."hadoop-conf".source = let
hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
in "${hadoopConf}";
variables.HADOOP_CONF_DIR = "/etc/hadoop-conf/";
};
};
}

View file

@ -0,0 +1,204 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.hadoop;
# Config files for hadoop services
hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
# Generator for HDFS service options
hadoopServiceOption = { serviceName, firewallOption ? true, extraOpts ? null }: {
enable = mkEnableOption serviceName;
restartIfChanged = mkOption {
type = types.bool;
description = ''
Automatically restart the service on config change.
This can be set to false to defer restarts on clusters running critical applications.
Please consider the security implications of inadvertently running an older version,
and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
'';
default = false;
};
extraFlags = mkOption{
type = with types; listOf str;
default = [];
description = "Extra command line flags to pass to ${serviceName}";
example = [
"-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.port=8010"
];
};
extraEnv = mkOption{
type = with types; attrsOf str;
default = {};
description = "Extra environment variables for ${serviceName}";
};
} // (optionalAttrs firewallOption {
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open firewall ports for ${serviceName}.";
};
}) // (optionalAttrs (extraOpts != null) extraOpts);
# Generator for HDFS service configs
hadoopServiceConfig =
{ name
, serviceOptions ? cfg.hdfs."${toLower name}"
, description ? "Hadoop HDFS ${name}"
, User ? "hdfs"
, allowedTCPPorts ? [ ]
, preStart ? ""
, environment ? { }
, extraConfig ? { }
}: (
mkIf serviceOptions.enable ( mkMerge [{
systemd.services."hdfs-${toLower name}" = {
inherit description preStart;
environment = environment // serviceOptions.extraEnv;
wantedBy = [ "multi-user.target" ];
inherit (serviceOptions) restartIfChanged;
serviceConfig = {
inherit User;
SyslogIdentifier = "hdfs-${toLower name}";
ExecStart = "${cfg.package}/bin/hdfs --config ${hadoopConf} ${toLower name} ${escapeShellArgs serviceOptions.extraFlags}";
Restart = "always";
};
};
services.hadoop.gatewayRole.enable = true;
networking.firewall.allowedTCPPorts = mkIf
((builtins.hasAttr "openFirewall" serviceOptions) && serviceOptions.openFirewall)
allowedTCPPorts;
} extraConfig])
);
in
{
options.services.hadoop.hdfs = {
namenode = hadoopServiceOption { serviceName = "HDFS NameNode"; } // {
formatOnInit = mkOption {
type = types.bool;
default = false;
description = ''
Format HDFS namenode on first start. This is useful for quickly spinning up
ephemeral HDFS clusters with a single namenode.
For HA clusters, initialization involves multiple steps across multiple nodes.
Follow this guide to initialize an HA cluster manually:
<link xlink:href="https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html"/>
'';
};
};
datanode = hadoopServiceOption { serviceName = "HDFS DataNode"; } // {
dataDirs = mkOption {
default = null;
description = "Tier and path definitions for datanode storage.";
type = with types; nullOr (listOf (submodule {
options = {
type = mkOption {
type = enum [ "SSD" "DISK" "ARCHIVE" "RAM_DISK" ];
description = ''
Storage types ([SSD]/[DISK]/[ARCHIVE]/[RAM_DISK]) for HDFS storage policies.
'';
};
path = mkOption {
type = path;
example = [ "/var/lib/hadoop/hdfs/dn" ];
description = "Determines where on the local filesystem a data node should store its blocks.";
};
};
}));
};
};
journalnode = hadoopServiceOption { serviceName = "HDFS JournalNode"; };
zkfc = hadoopServiceOption {
serviceName = "HDFS ZooKeeper failover controller";
firewallOption = false;
};
httpfs = hadoopServiceOption { serviceName = "HDFS JournalNode"; } // {
tempPath = mkOption {
type = types.path;
default = "/tmp/hadoop/httpfs";
description = "HTTPFS_TEMP path used by HTTPFS";
};
};
};
config = mkMerge [
(hadoopServiceConfig {
name = "NameNode";
allowedTCPPorts = [
9870 # namenode.http-address
8020 # namenode.rpc-address
8022 # namenode.servicerpc-address
8019 # dfs.ha.zkfc.port
];
preStart = (mkIf cfg.hdfs.namenode.formatOnInit
"${cfg.package}/bin/hdfs --config ${hadoopConf} namenode -format -nonInteractive || true"
);
})
(hadoopServiceConfig {
name = "DataNode";
# port numbers for datanode changed between hadoop 2 and 3
allowedTCPPorts = if versionAtLeast cfg.package.version "3" then [
9864 # datanode.http.address
9866 # datanode.address
9867 # datanode.ipc.address
] else [
50075 # datanode.http.address
50010 # datanode.address
50020 # datanode.ipc.address
];
extraConfig.services.hadoop.hdfsSiteInternal."dfs.datanode.data.dir" = let d = cfg.hdfs.datanode.dataDirs; in
if (d!= null) then (concatMapStringsSep "," (x: "["+x.type+"]file://"+x.path) cfg.hdfs.datanode.dataDirs) else d;
})
(hadoopServiceConfig {
name = "JournalNode";
allowedTCPPorts = [
8480 # dfs.journalnode.http-address
8485 # dfs.journalnode.rpc-address
];
})
(hadoopServiceConfig {
name = "zkfc";
description = "Hadoop HDFS ZooKeeper failover controller";
})
(hadoopServiceConfig {
name = "HTTPFS";
environment.HTTPFS_TEMP = cfg.hdfs.httpfs.tempPath;
preStart = "mkdir -p $HTTPFS_TEMP";
User = "httpfs";
allowedTCPPorts = [
14000 # httpfs.http.port
];
})
(mkIf cfg.gatewayRole.enable {
users.users.hdfs = {
description = "Hadoop HDFS user";
group = "hadoop";
uid = config.ids.uids.hdfs;
};
})
(mkIf cfg.hdfs.httpfs.enable {
users.users.httpfs = {
description = "Hadoop HTTPFS user";
group = "hadoop";
isSystemUser = true;
};
})
];
}

View file

@ -0,0 +1,200 @@
{ config, lib, pkgs, ...}:
with lib;
let
cfg = config.services.hadoop;
hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
restartIfChanged = mkOption {
type = types.bool;
description = ''
Automatically restart the service on config change.
This can be set to false to defer restarts on clusters running critical applications.
Please consider the security implications of inadvertently running an older version,
and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
'';
default = false;
};
extraFlags = mkOption{
type = with types; listOf str;
default = [];
description = "Extra command line flags to pass to the service";
example = [
"-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.port=8010"
];
};
extraEnv = mkOption{
type = with types; attrsOf str;
default = {};
description = "Extra environment variables";
};
in
{
options.services.hadoop.yarn = {
resourcemanager = {
enable = mkEnableOption "Hadoop YARN ResourceManager";
inherit restartIfChanged extraFlags extraEnv;
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open firewall ports for resourcemanager
'';
};
};
nodemanager = {
enable = mkEnableOption "Hadoop YARN NodeManager";
inherit restartIfChanged extraFlags extraEnv;
resource = {
cpuVCores = mkOption {
description = "Number of vcores that can be allocated for containers.";
type = with types; nullOr ints.positive;
default = null;
};
maximumAllocationVCores = mkOption {
description = "The maximum virtual CPU cores any container can be allocated.";
type = with types; nullOr ints.positive;
default = null;
};
memoryMB = mkOption {
description = "Amount of physical memory, in MB, that can be allocated for containers.";
type = with types; nullOr ints.positive;
default = null;
};
maximumAllocationMB = mkOption {
description = "The maximum physical memory any container can be allocated.";
type = with types; nullOr ints.positive;
default = null;
};
};
useCGroups = mkOption {
type = types.bool;
default = true;
description = ''
Use cgroups to enforce resource limits on containers
'';
};
localDir = mkOption {
description = "List of directories to store localized files in.";
type = with types; nullOr (listOf path);
example = [ "/var/lib/hadoop/yarn/nm" ];
default = null;
};
addBinBash = mkOption {
type = types.bool;
default = true;
description = ''
Add /bin/bash. This is needed by the linux container executor's launch script.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open firewall ports for nodemanager.
Because containers can listen on any ephemeral port, TCP ports 102465535 will be opened.
'';
};
};
};
config = mkMerge [
(mkIf cfg.gatewayRole.enable {
users.users.yarn = {
description = "Hadoop YARN user";
group = "hadoop";
uid = config.ids.uids.yarn;
};
})
(mkIf cfg.yarn.resourcemanager.enable {
systemd.services.yarn-resourcemanager = {
description = "Hadoop YARN ResourceManager";
wantedBy = [ "multi-user.target" ];
inherit (cfg.yarn.resourcemanager) restartIfChanged;
environment = cfg.yarn.resourcemanager.extraEnv;
serviceConfig = {
User = "yarn";
SyslogIdentifier = "yarn-resourcemanager";
ExecStart = "${cfg.package}/bin/yarn --config ${hadoopConf} " +
" resourcemanager ${escapeShellArgs cfg.yarn.resourcemanager.extraFlags}";
Restart = "always";
};
};
services.hadoop.gatewayRole.enable = true;
networking.firewall.allowedTCPPorts = (mkIf cfg.yarn.resourcemanager.openFirewall [
8088 # resourcemanager.webapp.address
8030 # resourcemanager.scheduler.address
8031 # resourcemanager.resource-tracker.address
8032 # resourcemanager.address
8033 # resourcemanager.admin.address
]);
})
(mkIf cfg.yarn.nodemanager.enable {
# Needed because yarn hardcodes /bin/bash in container start scripts
# These scripts can't be patched, they are generated at runtime
systemd.tmpfiles.rules = [
(mkIf cfg.yarn.nodemanager.addBinBash "L /bin/bash - - - - /run/current-system/sw/bin/bash")
];
systemd.services.yarn-nodemanager = {
description = "Hadoop YARN NodeManager";
wantedBy = [ "multi-user.target" ];
inherit (cfg.yarn.nodemanager) restartIfChanged;
environment = cfg.yarn.nodemanager.extraEnv;
preStart = ''
# create log dir
mkdir -p /var/log/hadoop/yarn/nodemanager
chown yarn:hadoop /var/log/hadoop/yarn/nodemanager
# set up setuid container executor binary
umount /run/wrappers/yarn-nodemanager/cgroup/cpu || true
rm -rf /run/wrappers/yarn-nodemanager/ || true
mkdir -p /run/wrappers/yarn-nodemanager/{bin,etc/hadoop,cgroup/cpu}
cp ${cfg.package}/lib/${cfg.package.untarDir}/bin/container-executor /run/wrappers/yarn-nodemanager/bin/
chgrp hadoop /run/wrappers/yarn-nodemanager/bin/container-executor
chmod 6050 /run/wrappers/yarn-nodemanager/bin/container-executor
cp ${hadoopConf}/container-executor.cfg /run/wrappers/yarn-nodemanager/etc/hadoop/
'';
serviceConfig = {
User = "yarn";
SyslogIdentifier = "yarn-nodemanager";
PermissionsStartOnly = true;
ExecStart = "${cfg.package}/bin/yarn --config ${hadoopConf} " +
" nodemanager ${escapeShellArgs cfg.yarn.nodemanager.extraFlags}";
Restart = "always";
};
};
services.hadoop.gatewayRole.enable = true;
services.hadoop.yarnSiteInternal = with cfg.yarn.nodemanager; {
"yarn.nodemanager.local-dirs" = localDir;
"yarn.scheduler.maximum-allocation-vcores" = resource.maximumAllocationVCores;
"yarn.scheduler.maximum-allocation-mb" = resource.maximumAllocationMB;
"yarn.nodemanager.resource.cpu-vcores" = resource.cpuVCores;
"yarn.nodemanager.resource.memory-mb" = resource.memoryMB;
} // mkIf useCGroups {
"yarn.nodemanager.linux-container-executor.cgroups.hierarchy" = "/hadoop-yarn";
"yarn.nodemanager.linux-container-executor.resources-handler.class" = "org.apache.hadoop.yarn.server.nodemanager.util.CgroupsLCEResourcesHandler";
"yarn.nodemanager.linux-container-executor.cgroups.mount" = "true";
"yarn.nodemanager.linux-container-executor.cgroups.mount-path" = "/run/wrappers/yarn-nodemanager/cgroup";
};
networking.firewall.allowedTCPPortRanges = [
(mkIf (cfg.yarn.nodemanager.openFirewall) {from = 1024; to = 65535;})
];
})
];
}

View file

@ -0,0 +1,128 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.k3s;
in
{
# interface
options.services.k3s = {
enable = mkEnableOption "k3s";
package = mkOption {
type = types.package;
default = pkgs.k3s;
defaultText = literalExpression "pkgs.k3s";
description = "Package that should be used for k3s";
};
role = mkOption {
description = ''
Whether k3s should run as a server or agent.
Note that the server, by default, also runs as an agent.
'';
default = "server";
type = types.enum [ "server" "agent" ];
};
serverAddr = mkOption {
type = types.str;
description = "The k3s server to connect to. This option only makes sense for an agent.";
example = "https://10.0.0.10:6443";
default = "";
};
token = mkOption {
type = types.str;
description = ''
The k3s token to use when connecting to the server. This option only makes sense for an agent.
WARNING: This option will expose store your token unencrypted world-readable in the nix store.
If this is undesired use the tokenFile option instead.
'';
default = "";
};
tokenFile = mkOption {
type = types.nullOr types.path;
description = "File path containing k3s token to use when connecting to the server. This option only makes sense for an agent.";
default = null;
};
docker = mkOption {
type = types.bool;
default = false;
description = "Use docker to run containers rather than the built-in containerd.";
};
extraFlags = mkOption {
description = "Extra flags to pass to the k3s command.";
type = types.str;
default = "";
example = "--no-deploy traefik --cluster-cidr 10.24.0.0/16";
};
disableAgent = mkOption {
type = types.bool;
default = false;
description = "Only run the server. This option only makes sense for a server.";
};
configPath = mkOption {
type = types.nullOr types.path;
default = null;
description = "File path containing the k3s YAML config. This is useful when the config is generated (for example on boot).";
};
};
# implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.role == "agent" -> (cfg.configPath != null || cfg.serverAddr != "");
message = "serverAddr or configPath (with 'server' key) should be set if role is 'agent'";
}
{
assertion = cfg.role == "agent" -> cfg.configPath != null || cfg.tokenFile != null || cfg.token != "";
message = "token or tokenFile or configPath (with 'token' or 'token-file' keys) should be set if role is 'agent'";
}
];
virtualisation.docker = mkIf cfg.docker {
enable = mkDefault true;
};
environment.systemPackages = [ config.services.k3s.package ];
systemd.services.k3s = {
description = "k3s service";
after = [ "network.service" "firewall.service" ] ++ (optional cfg.docker "docker.service");
wants = [ "network.service" "firewall.service" ];
wantedBy = [ "multi-user.target" ];
path = optional config.boot.zfs.enabled config.boot.zfs.package;
serviceConfig = {
# See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
Type = if cfg.role == "agent" then "exec" else "notify";
KillMode = "process";
Delegate = "yes";
Restart = "always";
RestartSec = "5s";
LimitNOFILE = 1048576;
LimitNPROC = "infinity";
LimitCORE = "infinity";
TasksMax = "infinity";
ExecStart = concatStringsSep " \\\n " (
[
"${cfg.package}/bin/k3s ${cfg.role}"
] ++ (optional cfg.docker "--docker")
++ (optional (cfg.docker && config.systemd.enableUnifiedCgroupHierarchy) "--kubelet-arg=cgroup-driver=systemd")
++ (optional cfg.disableAgent "--disable-agent")
++ (optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
++ (optional (cfg.token != "") "--token ${cfg.token}")
++ (optional (cfg.tokenFile != null) "--token-file ${cfg.tokenFile}")
++ (optional (cfg.configPath != null) "--config ${cfg.configPath}")
++ [ cfg.extraFlags ]
);
};
};
};
}

View file

@ -0,0 +1,171 @@
{ config, lib, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
cfg = top.addonManager;
isRBACEnabled = elem "RBAC" top.apiserver.authorizationMode;
addons = pkgs.runCommand "kubernetes-addons" { } ''
mkdir -p $out
# since we are mounting the addons to the addon manager, they need to be copied
${concatMapStringsSep ";" (a: "cp -v ${a}/* $out/") (mapAttrsToList (name: addon:
pkgs.writeTextDir "${name}.json" (builtins.toJSON addon)
) (cfg.addons))}
'';
in
{
###### interface
options.services.kubernetes.addonManager = with lib.types; {
bootstrapAddons = mkOption {
description = ''
Bootstrap addons are like regular addons, but they are applied with cluster-admin rigths.
They are applied at addon-manager startup only.
'';
default = { };
type = attrsOf attrs;
example = literalExpression ''
{
"my-service" = {
"apiVersion" = "v1";
"kind" = "Service";
"metadata" = {
"name" = "my-service";
"namespace" = "default";
};
"spec" = { ... };
};
}
'';
};
addons = mkOption {
description = "Kubernetes addons (any kind of Kubernetes resource can be an addon).";
default = { };
type = attrsOf (either attrs (listOf attrs));
example = literalExpression ''
{
"my-service" = {
"apiVersion" = "v1";
"kind" = "Service";
"metadata" = {
"name" = "my-service";
"namespace" = "default";
};
"spec" = { ... };
};
}
// import <nixpkgs/nixos/modules/services/cluster/kubernetes/dns.nix> { cfg = config.services.kubernetes; };
'';
};
enable = mkEnableOption "Kubernetes addon manager.";
};
###### implementation
config = mkIf cfg.enable {
environment.etc."kubernetes/addons".source = "${addons}/";
systemd.services.kube-addon-manager = {
description = "Kubernetes addon manager";
wantedBy = [ "kubernetes.target" ];
after = [ "kube-apiserver.service" ];
environment.ADDON_PATH = "/etc/kubernetes/addons/";
path = [ pkgs.gawk ];
serviceConfig = {
Slice = "kubernetes.slice";
ExecStart = "${top.package}/bin/kube-addons";
WorkingDirectory = top.dataDir;
User = "kubernetes";
Group = "kubernetes";
Restart = "on-failure";
RestartSec = 10;
};
unitConfig = {
StartLimitIntervalSec = 0;
};
};
services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled
(let
name = "system:kube-addon-manager";
namespace = "kube-system";
in
{
kube-addon-manager-r = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "Role";
metadata = {
inherit name namespace;
};
rules = [{
apiGroups = ["*"];
resources = ["*"];
verbs = ["*"];
}];
};
kube-addon-manager-rb = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "RoleBinding";
metadata = {
inherit name namespace;
};
roleRef = {
apiGroup = "rbac.authorization.k8s.io";
kind = "Role";
inherit name;
};
subjects = [{
apiGroup = "rbac.authorization.k8s.io";
kind = "User";
inherit name;
}];
};
kube-addon-manager-cluster-lister-cr = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRole";
metadata = {
name = "${name}:cluster-lister";
};
rules = [{
apiGroups = ["*"];
resources = ["*"];
verbs = ["list"];
}];
};
kube-addon-manager-cluster-lister-crb = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRoleBinding";
metadata = {
name = "${name}:cluster-lister";
};
roleRef = {
apiGroup = "rbac.authorization.k8s.io";
kind = "ClusterRole";
name = "${name}:cluster-lister";
};
subjects = [{
kind = "User";
inherit name;
}];
};
});
services.kubernetes.pki.certs = {
addonManager = top.lib.mkCert {
name = "kube-addon-manager";
CN = "system:kube-addon-manager";
action = "systemctl restart kube-addon-manager.service";
};
};
};
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,368 @@
{ config, options, pkgs, lib, ... }:
with lib;
let
version = "1.7.1";
cfg = config.services.kubernetes.addons.dns;
ports = {
dns = 10053;
health = 10054;
metrics = 10055;
};
in {
options.services.kubernetes.addons.dns = {
enable = mkEnableOption "kubernetes dns addon";
clusterIp = mkOption {
description = "Dns addon clusterIP";
# this default is also what kubernetes users
default = (
concatStringsSep "." (
take 3 (splitString "." config.services.kubernetes.apiserver.serviceClusterIpRange
))
) + ".254";
defaultText = literalDocBook ''
The <literal>x.y.z.254</literal> IP of
<literal>config.${options.services.kubernetes.apiserver.serviceClusterIpRange}</literal>.
'';
type = types.str;
};
clusterDomain = mkOption {
description = "Dns cluster domain";
default = "cluster.local";
type = types.str;
};
replicas = mkOption {
description = "Number of DNS pod replicas to deploy in the cluster.";
default = 2;
type = types.int;
};
reconcileMode = mkOption {
description = ''
Controls the addon manager reconciliation mode for the DNS addon.
Setting reconcile mode to EnsureExists makes it possible to tailor DNS behavior by editing the coredns ConfigMap.
See: <link xlink:href="https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/addon-manager/README.md"/>.
'';
default = "Reconcile";
type = types.enum [ "Reconcile" "EnsureExists" ];
};
coredns = mkOption {
description = "Docker image to seed for the CoreDNS container.";
type = types.attrs;
default = {
imageName = "coredns/coredns";
imageDigest = "sha256:4a6e0769130686518325b21b0c1d0688b54e7c79244d48e1b15634e98e40c6ef";
finalImageTag = version;
sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
};
};
corefile = mkOption {
description = ''
Custom coredns corefile configuration.
See: <link xlink:href="https://coredns.io/manual/toc/#configuration"/>.
'';
type = types.str;
default = ''
.:${toString ports.dns} {
errors
health :${toString ports.health}
kubernetes ${cfg.clusterDomain} in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :${toString ports.metrics}
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}'';
defaultText = literalExpression ''
'''
.:${toString ports.dns} {
errors
health :${toString ports.health}
kubernetes ''${config.services.kubernetes.addons.dns.clusterDomain} in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :${toString ports.metrics}
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
'''
'';
};
};
config = mkIf cfg.enable {
services.kubernetes.kubelet.seedDockerImages =
singleton (pkgs.dockerTools.pullImage cfg.coredns);
services.kubernetes.addonManager.bootstrapAddons = {
coredns-cr = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRole";
metadata = {
labels = {
"addonmanager.kubernetes.io/mode" = "Reconcile";
k8s-app = "kube-dns";
"kubernetes.io/cluster-service" = "true";
"kubernetes.io/bootstrapping" = "rbac-defaults";
};
name = "system:coredns";
};
rules = [
{
apiGroups = [ "" ];
resources = [ "endpoints" "services" "pods" "namespaces" ];
verbs = [ "list" "watch" ];
}
{
apiGroups = [ "" ];
resources = [ "nodes" ];
verbs = [ "get" ];
}
];
};
coredns-crb = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRoleBinding";
metadata = {
annotations = {
"rbac.authorization.kubernetes.io/autoupdate" = "true";
};
labels = {
"addonmanager.kubernetes.io/mode" = "Reconcile";
k8s-app = "kube-dns";
"kubernetes.io/cluster-service" = "true";
"kubernetes.io/bootstrapping" = "rbac-defaults";
};
name = "system:coredns";
};
roleRef = {
apiGroup = "rbac.authorization.k8s.io";
kind = "ClusterRole";
name = "system:coredns";
};
subjects = [
{
kind = "ServiceAccount";
name = "coredns";
namespace = "kube-system";
}
];
};
};
services.kubernetes.addonManager.addons = {
coredns-sa = {
apiVersion = "v1";
kind = "ServiceAccount";
metadata = {
labels = {
"addonmanager.kubernetes.io/mode" = "Reconcile";
k8s-app = "kube-dns";
"kubernetes.io/cluster-service" = "true";
};
name = "coredns";
namespace = "kube-system";
};
};
coredns-cm = {
apiVersion = "v1";
kind = "ConfigMap";
metadata = {
labels = {
"addonmanager.kubernetes.io/mode" = cfg.reconcileMode;
k8s-app = "kube-dns";
"kubernetes.io/cluster-service" = "true";
};
name = "coredns";
namespace = "kube-system";
};
data = {
Corefile = cfg.corefile;
};
};
coredns-deploy = {
apiVersion = "apps/v1";
kind = "Deployment";
metadata = {
labels = {
"addonmanager.kubernetes.io/mode" = cfg.reconcileMode;
k8s-app = "kube-dns";
"kubernetes.io/cluster-service" = "true";
"kubernetes.io/name" = "CoreDNS";
};
name = "coredns";
namespace = "kube-system";
};
spec = {
replicas = cfg.replicas;
selector = {
matchLabels = { k8s-app = "kube-dns"; };
};
strategy = {
rollingUpdate = { maxUnavailable = 1; };
type = "RollingUpdate";
};
template = {
metadata = {
labels = {
k8s-app = "kube-dns";
};
};
spec = {
containers = [
{
args = [ "-conf" "/etc/coredns/Corefile" ];
image = with cfg.coredns; "${imageName}:${finalImageTag}";
imagePullPolicy = "Never";
livenessProbe = {
failureThreshold = 5;
httpGet = {
path = "/health";
port = ports.health;
scheme = "HTTP";
};
initialDelaySeconds = 60;
successThreshold = 1;
timeoutSeconds = 5;
};
name = "coredns";
ports = [
{
containerPort = ports.dns;
name = "dns";
protocol = "UDP";
}
{
containerPort = ports.dns;
name = "dns-tcp";
protocol = "TCP";
}
{
containerPort = ports.metrics;
name = "metrics";
protocol = "TCP";
}
];
resources = {
limits = {
memory = "170Mi";
};
requests = {
cpu = "100m";
memory = "70Mi";
};
};
securityContext = {
allowPrivilegeEscalation = false;
capabilities = {
drop = [ "all" ];
};
readOnlyRootFilesystem = true;
};
volumeMounts = [
{
mountPath = "/etc/coredns";
name = "config-volume";
readOnly = true;
}
];
}
];
dnsPolicy = "Default";
nodeSelector = {
"beta.kubernetes.io/os" = "linux";
};
serviceAccountName = "coredns";
tolerations = [
{
effect = "NoSchedule";
key = "node-role.kubernetes.io/master";
}
{
key = "CriticalAddonsOnly";
operator = "Exists";
}
];
volumes = [
{
configMap = {
items = [
{
key = "Corefile";
path = "Corefile";
}
];
name = "coredns";
};
name = "config-volume";
}
];
};
};
};
};
coredns-svc = {
apiVersion = "v1";
kind = "Service";
metadata = {
annotations = {
"prometheus.io/port" = toString ports.metrics;
"prometheus.io/scrape" = "true";
};
labels = {
"addonmanager.kubernetes.io/mode" = "Reconcile";
k8s-app = "kube-dns";
"kubernetes.io/cluster-service" = "true";
"kubernetes.io/name" = "CoreDNS";
};
name = "kube-dns";
namespace = "kube-system";
};
spec = {
clusterIP = cfg.clusterIp;
ports = [
{
name = "dns";
port = 53;
targetPort = ports.dns;
protocol = "UDP";
}
{
name = "dns-tcp";
port = 53;
targetPort = ports.dns;
protocol = "TCP";
}
];
selector = { k8s-app = "kube-dns"; };
};
};
};
services.kubernetes.kubelet.clusterDns = mkDefault cfg.clusterIp;
};
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,500 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
otop = options.services.kubernetes;
cfg = top.apiserver;
isRBACEnabled = elem "RBAC" cfg.authorizationMode;
apiserverServiceIP = (concatStringsSep "." (
take 3 (splitString "." cfg.serviceClusterIpRange
)) + ".1");
in
{
imports = [
(mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "admissionControl" ] [ "services" "kubernetes" "apiserver" "enableAdmissionPlugins" ])
(mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "address" ] ["services" "kubernetes" "apiserver" "bindAddress"])
(mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "port" ] ["services" "kubernetes" "apiserver" "insecurePort"])
(mkRemovedOptionModule [ "services" "kubernetes" "apiserver" "publicAddress" ] "")
(mkRenamedOptionModule [ "services" "kubernetes" "etcd" "servers" ] [ "services" "kubernetes" "apiserver" "etcd" "servers" ])
(mkRenamedOptionModule [ "services" "kubernetes" "etcd" "keyFile" ] [ "services" "kubernetes" "apiserver" "etcd" "keyFile" ])
(mkRenamedOptionModule [ "services" "kubernetes" "etcd" "certFile" ] [ "services" "kubernetes" "apiserver" "etcd" "certFile" ])
(mkRenamedOptionModule [ "services" "kubernetes" "etcd" "caFile" ] [ "services" "kubernetes" "apiserver" "etcd" "caFile" ])
];
###### interface
options.services.kubernetes.apiserver = with lib.types; {
advertiseAddress = mkOption {
description = ''
Kubernetes apiserver IP address on which to advertise the apiserver
to members of the cluster. This address must be reachable by the rest
of the cluster.
'';
default = null;
type = nullOr str;
};
allowPrivileged = mkOption {
description = "Whether to allow privileged containers on Kubernetes.";
default = false;
type = bool;
};
authorizationMode = mkOption {
description = ''
Kubernetes apiserver authorization mode (AlwaysAllow/AlwaysDeny/ABAC/Webhook/RBAC/Node). See
<link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
'';
default = ["RBAC" "Node"]; # Enabling RBAC by default, although kubernetes default is AllowAllow
type = listOf (enum ["AlwaysAllow" "AlwaysDeny" "ABAC" "Webhook" "RBAC" "Node"]);
};
authorizationPolicy = mkOption {
description = ''
Kubernetes apiserver authorization policy file. See
<link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
'';
default = [];
type = listOf attrs;
};
basicAuthFile = mkOption {
description = ''
Kubernetes apiserver basic authentication file. See
<link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
'';
default = null;
type = nullOr path;
};
bindAddress = mkOption {
description = ''
The IP address on which to listen for the --secure-port port.
The associated interface(s) must be reachable by the rest
of the cluster, and by CLI/web clients.
'';
default = "0.0.0.0";
type = str;
};
clientCaFile = mkOption {
description = "Kubernetes apiserver CA file for client auth.";
default = top.caFile;
defaultText = literalExpression "config.${otop.caFile}";
type = nullOr path;
};
disableAdmissionPlugins = mkOption {
description = ''
Kubernetes admission control plugins to disable. See
<link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
'';
default = [];
type = listOf str;
};
enable = mkEnableOption "Kubernetes apiserver";
enableAdmissionPlugins = mkOption {
description = ''
Kubernetes admission control plugins to enable. See
<link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
'';
default = [
"NamespaceLifecycle" "LimitRanger" "ServiceAccount"
"ResourceQuota" "DefaultStorageClass" "DefaultTolerationSeconds"
"NodeRestriction"
];
example = [
"NamespaceLifecycle" "NamespaceExists" "LimitRanger"
"SecurityContextDeny" "ServiceAccount" "ResourceQuota"
"PodSecurityPolicy" "NodeRestriction" "DefaultStorageClass"
];
type = listOf str;
};
etcd = {
servers = mkOption {
description = "List of etcd servers.";
default = ["http://127.0.0.1:2379"];
type = types.listOf types.str;
};
keyFile = mkOption {
description = "Etcd key file.";
default = null;
type = types.nullOr types.path;
};
certFile = mkOption {
description = "Etcd cert file.";
default = null;
type = types.nullOr types.path;
};
caFile = mkOption {
description = "Etcd ca file.";
default = top.caFile;
defaultText = literalExpression "config.${otop.caFile}";
type = types.nullOr types.path;
};
};
extraOpts = mkOption {
description = "Kubernetes apiserver extra command line options.";
default = "";
type = separatedString " ";
};
extraSANs = mkOption {
description = "Extra x509 Subject Alternative Names to be added to the kubernetes apiserver tls cert.";
default = [];
type = listOf str;
};
featureGates = mkOption {
description = "List set of feature gates";
default = top.featureGates;
defaultText = literalExpression "config.${otop.featureGates}";
type = listOf str;
};
insecureBindAddress = mkOption {
description = "The IP address on which to serve the --insecure-port.";
default = "127.0.0.1";
type = str;
};
insecurePort = mkOption {
description = "Kubernetes apiserver insecure listening port. (0 = disabled)";
default = 0;
type = int;
};
kubeletClientCaFile = mkOption {
description = "Path to a cert file for connecting to kubelet.";
default = top.caFile;
defaultText = literalExpression "config.${otop.caFile}";
type = nullOr path;
};
kubeletClientCertFile = mkOption {
description = "Client certificate to use for connections to kubelet.";
default = null;
type = nullOr path;
};
kubeletClientKeyFile = mkOption {
description = "Key to use for connections to kubelet.";
default = null;
type = nullOr path;
};
preferredAddressTypes = mkOption {
description = "List of the preferred NodeAddressTypes to use for kubelet connections.";
type = nullOr str;
default = null;
};
proxyClientCertFile = mkOption {
description = "Client certificate to use for connections to proxy.";
default = null;
type = nullOr path;
};
proxyClientKeyFile = mkOption {
description = "Key to use for connections to proxy.";
default = null;
type = nullOr path;
};
runtimeConfig = mkOption {
description = ''
Api runtime configuration. See
<link xlink:href="https://kubernetes.io/docs/tasks/administer-cluster/cluster-management/"/>
'';
default = "authentication.k8s.io/v1beta1=true";
example = "api/all=false,api/v1=true";
type = str;
};
storageBackend = mkOption {
description = ''
Kubernetes apiserver storage backend.
'';
default = "etcd3";
type = enum ["etcd2" "etcd3"];
};
securePort = mkOption {
description = "Kubernetes apiserver secure port.";
default = 6443;
type = int;
};
apiAudiences = mkOption {
description = ''
Kubernetes apiserver ServiceAccount issuer.
'';
default = "api,https://kubernetes.default.svc";
type = str;
};
serviceAccountIssuer = mkOption {
description = ''
Kubernetes apiserver ServiceAccount issuer.
'';
default = "https://kubernetes.default.svc";
type = str;
};
serviceAccountSigningKeyFile = mkOption {
description = ''
Path to the file that contains the current private key of the service
account token issuer. The issuer will sign issued ID tokens with this
private key.
'';
type = path;
};
serviceAccountKeyFile = mkOption {
description = ''
File containing PEM-encoded x509 RSA or ECDSA private or public keys,
used to verify ServiceAccount tokens. The specified file can contain
multiple keys, and the flag can be specified multiple times with
different files. If unspecified, --tls-private-key-file is used.
Must be specified when --service-account-signing-key is provided
'';
type = path;
};
serviceClusterIpRange = mkOption {
description = ''
A CIDR notation IP range from which to assign service cluster IPs.
This must not overlap with any IP ranges assigned to nodes for pods.
'';
default = "10.0.0.0/24";
type = str;
};
tlsCertFile = mkOption {
description = "Kubernetes apiserver certificate file.";
default = null;
type = nullOr path;
};
tlsKeyFile = mkOption {
description = "Kubernetes apiserver private key file.";
default = null;
type = nullOr path;
};
tokenAuthFile = mkOption {
description = ''
Kubernetes apiserver token authentication file. See
<link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
'';
default = null;
type = nullOr path;
};
verbosity = mkOption {
description = ''
Optional glog verbosity level for logging statements. See
<link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
'';
default = null;
type = nullOr int;
};
webhookConfig = mkOption {
description = ''
Kubernetes apiserver Webhook config file. It uses the kubeconfig file format.
See <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/webhook/"/>
'';
default = null;
type = nullOr path;
};
};
###### implementation
config = mkMerge [
(mkIf cfg.enable {
systemd.services.kube-apiserver = {
description = "Kubernetes APIServer Service";
wantedBy = [ "kubernetes.target" ];
after = [ "network.target" ];
serviceConfig = {
Slice = "kubernetes.slice";
ExecStart = ''${top.package}/bin/kube-apiserver \
--allow-privileged=${boolToString cfg.allowPrivileged} \
--authorization-mode=${concatStringsSep "," cfg.authorizationMode} \
${optionalString (elem "ABAC" cfg.authorizationMode)
"--authorization-policy-file=${
pkgs.writeText "kube-auth-policy.jsonl"
(concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.authorizationPolicy)
}"
} \
${optionalString (elem "Webhook" cfg.authorizationMode)
"--authorization-webhook-config-file=${cfg.webhookConfig}"
} \
--bind-address=${cfg.bindAddress} \
${optionalString (cfg.advertiseAddress != null)
"--advertise-address=${cfg.advertiseAddress}"} \
${optionalString (cfg.clientCaFile != null)
"--client-ca-file=${cfg.clientCaFile}"} \
--disable-admission-plugins=${concatStringsSep "," cfg.disableAdmissionPlugins} \
--enable-admission-plugins=${concatStringsSep "," cfg.enableAdmissionPlugins} \
--etcd-servers=${concatStringsSep "," cfg.etcd.servers} \
${optionalString (cfg.etcd.caFile != null)
"--etcd-cafile=${cfg.etcd.caFile}"} \
${optionalString (cfg.etcd.certFile != null)
"--etcd-certfile=${cfg.etcd.certFile}"} \
${optionalString (cfg.etcd.keyFile != null)
"--etcd-keyfile=${cfg.etcd.keyFile}"} \
${optionalString (cfg.featureGates != [])
"--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
${optionalString (cfg.basicAuthFile != null)
"--basic-auth-file=${cfg.basicAuthFile}"} \
${optionalString (cfg.kubeletClientCaFile != null)
"--kubelet-certificate-authority=${cfg.kubeletClientCaFile}"} \
${optionalString (cfg.kubeletClientCertFile != null)
"--kubelet-client-certificate=${cfg.kubeletClientCertFile}"} \
${optionalString (cfg.kubeletClientKeyFile != null)
"--kubelet-client-key=${cfg.kubeletClientKeyFile}"} \
${optionalString (cfg.preferredAddressTypes != null)
"--kubelet-preferred-address-types=${cfg.preferredAddressTypes}"} \
${optionalString (cfg.proxyClientCertFile != null)
"--proxy-client-cert-file=${cfg.proxyClientCertFile}"} \
${optionalString (cfg.proxyClientKeyFile != null)
"--proxy-client-key-file=${cfg.proxyClientKeyFile}"} \
--insecure-bind-address=${cfg.insecureBindAddress} \
--insecure-port=${toString cfg.insecurePort} \
${optionalString (cfg.runtimeConfig != "")
"--runtime-config=${cfg.runtimeConfig}"} \
--secure-port=${toString cfg.securePort} \
--api-audiences=${toString cfg.apiAudiences} \
--service-account-issuer=${toString cfg.serviceAccountIssuer} \
--service-account-signing-key-file=${cfg.serviceAccountSigningKeyFile} \
--service-account-key-file=${cfg.serviceAccountKeyFile} \
--service-cluster-ip-range=${cfg.serviceClusterIpRange} \
--storage-backend=${cfg.storageBackend} \
${optionalString (cfg.tlsCertFile != null)
"--tls-cert-file=${cfg.tlsCertFile}"} \
${optionalString (cfg.tlsKeyFile != null)
"--tls-private-key-file=${cfg.tlsKeyFile}"} \
${optionalString (cfg.tokenAuthFile != null)
"--token-auth-file=${cfg.tokenAuthFile}"} \
${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
${cfg.extraOpts}
'';
WorkingDirectory = top.dataDir;
User = "kubernetes";
Group = "kubernetes";
AmbientCapabilities = "cap_net_bind_service";
Restart = "on-failure";
RestartSec = 5;
};
unitConfig = {
StartLimitIntervalSec = 0;
};
};
services.etcd = {
clientCertAuth = mkDefault true;
peerClientCertAuth = mkDefault true;
listenClientUrls = mkDefault ["https://0.0.0.0:2379"];
listenPeerUrls = mkDefault ["https://0.0.0.0:2380"];
advertiseClientUrls = mkDefault ["https://${top.masterAddress}:2379"];
initialCluster = mkDefault ["${top.masterAddress}=https://${top.masterAddress}:2380"];
name = mkDefault top.masterAddress;
initialAdvertisePeerUrls = mkDefault ["https://${top.masterAddress}:2380"];
};
services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled {
apiserver-kubelet-api-admin-crb = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRoleBinding";
metadata = {
name = "system:kube-apiserver:kubelet-api-admin";
};
roleRef = {
apiGroup = "rbac.authorization.k8s.io";
kind = "ClusterRole";
name = "system:kubelet-api-admin";
};
subjects = [{
kind = "User";
name = "system:kube-apiserver";
}];
};
};
services.kubernetes.pki.certs = with top.lib; {
apiServer = mkCert {
name = "kube-apiserver";
CN = "kubernetes";
hosts = [
"kubernetes.default.svc"
"kubernetes.default.svc.${top.addons.dns.clusterDomain}"
cfg.advertiseAddress
top.masterAddress
apiserverServiceIP
"127.0.0.1"
] ++ cfg.extraSANs;
action = "systemctl restart kube-apiserver.service";
};
apiserverProxyClient = mkCert {
name = "kube-apiserver-proxy-client";
CN = "front-proxy-client";
action = "systemctl restart kube-apiserver.service";
};
apiserverKubeletClient = mkCert {
name = "kube-apiserver-kubelet-client";
CN = "system:kube-apiserver";
action = "systemctl restart kube-apiserver.service";
};
apiserverEtcdClient = mkCert {
name = "kube-apiserver-etcd-client";
CN = "etcd-client";
action = "systemctl restart kube-apiserver.service";
};
clusterAdmin = mkCert {
name = "cluster-admin";
CN = "cluster-admin";
fields = {
O = "system:masters";
};
privateKeyOwner = "root";
};
etcd = mkCert {
name = "etcd";
CN = top.masterAddress;
hosts = [
"etcd.local"
"etcd.${top.addons.dns.clusterDomain}"
top.masterAddress
cfg.advertiseAddress
];
privateKeyOwner = "etcd";
action = "systemctl restart etcd.service";
};
};
})
];
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,176 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
otop = options.services.kubernetes;
cfg = top.controllerManager;
in
{
imports = [
(mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "address" ] ["services" "kubernetes" "controllerManager" "bindAddress"])
(mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "port" ] ["services" "kubernetes" "controllerManager" "insecurePort"])
];
###### interface
options.services.kubernetes.controllerManager = with lib.types; {
allocateNodeCIDRs = mkOption {
description = "Whether to automatically allocate CIDR ranges for cluster nodes.";
default = true;
type = bool;
};
bindAddress = mkOption {
description = "Kubernetes controller manager listening address.";
default = "127.0.0.1";
type = str;
};
clusterCidr = mkOption {
description = "Kubernetes CIDR Range for Pods in cluster.";
default = top.clusterCidr;
defaultText = literalExpression "config.${otop.clusterCidr}";
type = str;
};
enable = mkEnableOption "Kubernetes controller manager";
extraOpts = mkOption {
description = "Kubernetes controller manager extra command line options.";
default = "";
type = separatedString " ";
};
featureGates = mkOption {
description = "List set of feature gates";
default = top.featureGates;
defaultText = literalExpression "config.${otop.featureGates}";
type = listOf str;
};
insecurePort = mkOption {
description = "Kubernetes controller manager insecure listening port.";
default = 0;
type = int;
};
kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes controller manager";
leaderElect = mkOption {
description = "Whether to start leader election before executing main loop.";
type = bool;
default = true;
};
rootCaFile = mkOption {
description = ''
Kubernetes controller manager certificate authority file included in
service account's token secret.
'';
default = top.caFile;
defaultText = literalExpression "config.${otop.caFile}";
type = nullOr path;
};
securePort = mkOption {
description = "Kubernetes controller manager secure listening port.";
default = 10252;
type = int;
};
serviceAccountKeyFile = mkOption {
description = ''
Kubernetes controller manager PEM-encoded private RSA key file used to
sign service account tokens
'';
default = null;
type = nullOr path;
};
tlsCertFile = mkOption {
description = "Kubernetes controller-manager certificate file.";
default = null;
type = nullOr path;
};
tlsKeyFile = mkOption {
description = "Kubernetes controller-manager private key file.";
default = null;
type = nullOr path;
};
verbosity = mkOption {
description = ''
Optional glog verbosity level for logging statements. See
<link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
'';
default = null;
type = nullOr int;
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.kube-controller-manager = {
description = "Kubernetes Controller Manager Service";
wantedBy = [ "kubernetes.target" ];
after = [ "kube-apiserver.service" ];
serviceConfig = {
RestartSec = "30s";
Restart = "on-failure";
Slice = "kubernetes.slice";
ExecStart = ''${top.package}/bin/kube-controller-manager \
--allocate-node-cidrs=${boolToString cfg.allocateNodeCIDRs} \
--bind-address=${cfg.bindAddress} \
${optionalString (cfg.clusterCidr!=null)
"--cluster-cidr=${cfg.clusterCidr}"} \
${optionalString (cfg.featureGates != [])
"--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
--kubeconfig=${top.lib.mkKubeConfig "kube-controller-manager" cfg.kubeconfig} \
--leader-elect=${boolToString cfg.leaderElect} \
${optionalString (cfg.rootCaFile!=null)
"--root-ca-file=${cfg.rootCaFile}"} \
--port=${toString cfg.insecurePort} \
--secure-port=${toString cfg.securePort} \
${optionalString (cfg.serviceAccountKeyFile!=null)
"--service-account-private-key-file=${cfg.serviceAccountKeyFile}"} \
${optionalString (cfg.tlsCertFile!=null)
"--tls-cert-file=${cfg.tlsCertFile}"} \
${optionalString (cfg.tlsKeyFile!=null)
"--tls-private-key-file=${cfg.tlsKeyFile}"} \
${optionalString (elem "RBAC" top.apiserver.authorizationMode)
"--use-service-account-credentials"} \
${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
${cfg.extraOpts}
'';
WorkingDirectory = top.dataDir;
User = "kubernetes";
Group = "kubernetes";
};
unitConfig = {
StartLimitIntervalSec = 0;
};
path = top.path;
};
services.kubernetes.pki.certs = with top.lib; {
controllerManager = mkCert {
name = "kube-controller-manager";
CN = "kube-controller-manager";
action = "systemctl restart kube-controller-manager.service";
};
controllerManagerClient = mkCert {
name = "kube-controller-manager-client";
CN = "system:kube-controller-manager";
action = "systemctl restart kube-controller-manager.service";
};
};
services.kubernetes.controllerManager.kubeconfig.server = mkDefault top.apiserverAddress;
};
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,315 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.kubernetes;
opt = options.services.kubernetes;
defaultContainerdSettings = {
version = 2;
root = "/var/lib/containerd";
state = "/run/containerd";
oom_score = 0;
grpc = {
address = "/run/containerd/containerd.sock";
};
plugins."io.containerd.grpc.v1.cri" = {
sandbox_image = "pause:latest";
cni = {
bin_dir = "/opt/cni/bin";
max_conf_num = 0;
};
containerd.runtimes.runc = {
runtime_type = "io.containerd.runc.v2";
options.SystemdCgroup = true;
};
};
};
mkKubeConfig = name: conf: pkgs.writeText "${name}-kubeconfig" (builtins.toJSON {
apiVersion = "v1";
kind = "Config";
clusters = [{
name = "local";
cluster.certificate-authority = conf.caFile or cfg.caFile;
cluster.server = conf.server;
}];
users = [{
inherit name;
user = {
client-certificate = conf.certFile;
client-key = conf.keyFile;
};
}];
contexts = [{
context = {
cluster = "local";
user = name;
};
name = "local";
}];
current-context = "local";
});
caCert = secret "ca";
etcdEndpoints = ["https://${cfg.masterAddress}:2379"];
mkCert = { name, CN, hosts ? [], fields ? {}, action ? "",
privateKeyOwner ? "kubernetes" }: rec {
inherit name caCert CN hosts fields action;
cert = secret name;
key = secret "${name}-key";
privateKeyOptions = {
owner = privateKeyOwner;
group = "nogroup";
mode = "0600";
path = key;
};
};
secret = name: "${cfg.secretsPath}/${name}.pem";
mkKubeConfigOptions = prefix: {
server = mkOption {
description = "${prefix} kube-apiserver server address.";
type = types.str;
};
caFile = mkOption {
description = "${prefix} certificate authority file used to connect to kube-apiserver.";
type = types.nullOr types.path;
default = cfg.caFile;
defaultText = literalExpression "config.${opt.caFile}";
};
certFile = mkOption {
description = "${prefix} client certificate file used to connect to kube-apiserver.";
type = types.nullOr types.path;
default = null;
};
keyFile = mkOption {
description = "${prefix} client key file used to connect to kube-apiserver.";
type = types.nullOr types.path;
default = null;
};
};
in {
imports = [
(mkRemovedOptionModule [ "services" "kubernetes" "addons" "dashboard" ] "Removed due to it being an outdated version")
(mkRemovedOptionModule [ "services" "kubernetes" "verbose" ] "")
];
###### interface
options.services.kubernetes = {
roles = mkOption {
description = ''
Kubernetes role that this machine should take.
Master role will enable etcd, apiserver, scheduler, controller manager
addon manager, flannel and proxy services.
Node role will enable flannel, docker, kubelet and proxy services.
'';
default = [];
type = types.listOf (types.enum ["master" "node"]);
};
package = mkOption {
description = "Kubernetes package to use.";
type = types.package;
default = pkgs.kubernetes;
defaultText = literalExpression "pkgs.kubernetes";
};
kubeconfig = mkKubeConfigOptions "Default kubeconfig";
apiserverAddress = mkOption {
description = ''
Clusterwide accessible address for the kubernetes apiserver,
including protocol and optional port.
'';
example = "https://kubernetes-apiserver.example.com:6443";
type = types.str;
};
caFile = mkOption {
description = "Default kubernetes certificate authority";
type = types.nullOr types.path;
default = null;
};
dataDir = mkOption {
description = "Kubernetes root directory for managing kubelet files.";
default = "/var/lib/kubernetes";
type = types.path;
};
easyCerts = mkOption {
description = "Automatically setup x509 certificates and keys for the entire cluster.";
default = false;
type = types.bool;
};
featureGates = mkOption {
description = "List set of feature gates.";
default = [];
type = types.listOf types.str;
};
masterAddress = mkOption {
description = "Clusterwide available network address or hostname for the kubernetes master server.";
example = "master.example.com";
type = types.str;
};
path = mkOption {
description = "Packages added to the services' PATH environment variable. Both the bin and sbin subdirectories of each package are added.";
type = types.listOf types.package;
default = [];
};
clusterCidr = mkOption {
description = "Kubernetes controller manager and proxy CIDR Range for Pods in cluster.";
default = "10.1.0.0/16";
type = types.nullOr types.str;
};
lib = mkOption {
description = "Common functions for the kubernetes modules.";
default = {
inherit mkCert;
inherit mkKubeConfig;
inherit mkKubeConfigOptions;
};
type = types.attrs;
};
secretsPath = mkOption {
description = "Default location for kubernetes secrets. Not a store location.";
type = types.path;
default = cfg.dataDir + "/secrets";
defaultText = literalExpression ''
config.${opt.dataDir} + "/secrets"
'';
};
};
###### implementation
config = mkMerge [
(mkIf cfg.easyCerts {
services.kubernetes.pki.enable = mkDefault true;
services.kubernetes.caFile = caCert;
})
(mkIf (elem "master" cfg.roles) {
services.kubernetes.apiserver.enable = mkDefault true;
services.kubernetes.scheduler.enable = mkDefault true;
services.kubernetes.controllerManager.enable = mkDefault true;
services.kubernetes.addonManager.enable = mkDefault true;
services.kubernetes.proxy.enable = mkDefault true;
services.etcd.enable = true; # Cannot mkDefault because of flannel default options
services.kubernetes.kubelet = {
enable = mkDefault true;
taints = mkIf (!(elem "node" cfg.roles)) {
master = {
key = "node-role.kubernetes.io/master";
value = "true";
effect = "NoSchedule";
};
};
};
})
(mkIf (all (el: el == "master") cfg.roles) {
# if this node is only a master make it unschedulable by default
services.kubernetes.kubelet.unschedulable = mkDefault true;
})
(mkIf (elem "node" cfg.roles) {
services.kubernetes.kubelet.enable = mkDefault true;
services.kubernetes.proxy.enable = mkDefault true;
})
# Using "services.kubernetes.roles" will automatically enable easyCerts and flannel
(mkIf (cfg.roles != []) {
services.kubernetes.flannel.enable = mkDefault true;
services.flannel.etcd.endpoints = mkDefault etcdEndpoints;
services.kubernetes.easyCerts = mkDefault true;
})
(mkIf cfg.apiserver.enable {
services.kubernetes.pki.etcClusterAdminKubeconfig = mkDefault "kubernetes/cluster-admin.kubeconfig";
services.kubernetes.apiserver.etcd.servers = mkDefault etcdEndpoints;
})
(mkIf cfg.kubelet.enable {
virtualisation.containerd = {
enable = mkDefault true;
settings = mapAttrsRecursive (name: mkDefault) defaultContainerdSettings;
};
})
(mkIf (cfg.apiserver.enable || cfg.controllerManager.enable) {
services.kubernetes.pki.certs = {
serviceAccount = mkCert {
name = "service-account";
CN = "system:service-account-signer";
action = ''
systemctl reload \
kube-apiserver.service \
kube-controller-manager.service
'';
};
};
})
(mkIf (
cfg.apiserver.enable ||
cfg.scheduler.enable ||
cfg.controllerManager.enable ||
cfg.kubelet.enable ||
cfg.proxy.enable ||
cfg.addonManager.enable
) {
systemd.targets.kubernetes = {
description = "Kubernetes";
wantedBy = [ "multi-user.target" ];
};
systemd.tmpfiles.rules = [
"d /opt/cni/bin 0755 root root -"
"d /run/kubernetes 0755 kubernetes kubernetes -"
"d /var/lib/kubernetes 0755 kubernetes kubernetes -"
];
users.users.kubernetes = {
uid = config.ids.uids.kubernetes;
description = "Kubernetes user";
group = "kubernetes";
home = cfg.dataDir;
createHome = true;
};
users.groups.kubernetes.gid = config.ids.gids.kubernetes;
# dns addon is enabled by default
services.kubernetes.addons.dns.enable = mkDefault true;
services.kubernetes.apiserverAddress = mkDefault ("https://${if cfg.apiserver.advertiseAddress != null
then cfg.apiserver.advertiseAddress
else "${cfg.masterAddress}:${toString cfg.apiserver.securePort}"}");
})
];
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,100 @@
{ config, lib, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
cfg = top.flannel;
# we want flannel to use kubernetes itself as configuration backend, not direct etcd
storageBackend = "kubernetes";
in
{
###### interface
options.services.kubernetes.flannel = {
enable = mkEnableOption "enable flannel networking";
};
###### implementation
config = mkIf cfg.enable {
services.flannel = {
enable = mkDefault true;
network = mkDefault top.clusterCidr;
inherit storageBackend;
nodeName = config.services.kubernetes.kubelet.hostname;
};
services.kubernetes.kubelet = {
networkPlugin = mkDefault "cni";
cni.config = mkDefault [{
name = "mynet";
type = "flannel";
cniVersion = "0.3.1";
delegate = {
isDefaultGateway = true;
bridge = "mynet";
};
}];
};
networking = {
firewall.allowedUDPPorts = [
8285 # flannel udp
8472 # flannel vxlan
];
dhcpcd.denyInterfaces = [ "mynet*" "flannel*" ];
};
services.kubernetes.pki.certs = {
flannelClient = top.lib.mkCert {
name = "flannel-client";
CN = "flannel-client";
action = "systemctl restart flannel.service";
};
};
# give flannel som kubernetes rbac permissions if applicable
services.kubernetes.addonManager.bootstrapAddons = mkIf ((storageBackend == "kubernetes") && (elem "RBAC" top.apiserver.authorizationMode)) {
flannel-cr = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRole";
metadata = { name = "flannel"; };
rules = [{
apiGroups = [ "" ];
resources = [ "pods" ];
verbs = [ "get" ];
}
{
apiGroups = [ "" ];
resources = [ "nodes" ];
verbs = [ "list" "watch" ];
}
{
apiGroups = [ "" ];
resources = [ "nodes/status" ];
verbs = [ "patch" ];
}];
};
flannel-crb = {
apiVersion = "rbac.authorization.k8s.io/v1";
kind = "ClusterRoleBinding";
metadata = { name = "flannel"; };
roleRef = {
apiGroup = "rbac.authorization.k8s.io";
kind = "ClusterRole";
name = "flannel";
};
subjects = [{
kind = "User";
name = "flannel-client";
}];
};
};
};
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,398 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
otop = options.services.kubernetes;
cfg = top.kubelet;
cniConfig =
if cfg.cni.config != [] && cfg.cni.configDir != null then
throw "Verbatim CNI-config and CNI configDir cannot both be set."
else if cfg.cni.configDir != null then
cfg.cni.configDir
else
(pkgs.buildEnv {
name = "kubernetes-cni-config";
paths = imap (i: entry:
pkgs.writeTextDir "${toString (10+i)}-${entry.type}.conf" (builtins.toJSON entry)
) cfg.cni.config;
});
infraContainer = pkgs.dockerTools.buildImage {
name = "pause";
tag = "latest";
contents = top.package.pause;
config.Cmd = ["/bin/pause"];
};
kubeconfig = top.lib.mkKubeConfig "kubelet" cfg.kubeconfig;
manifestPath = "kubernetes/manifests";
taintOptions = with lib.types; { name, ... }: {
options = {
key = mkOption {
description = "Key of taint.";
default = name;
defaultText = literalDocBook "Name of this submodule.";
type = str;
};
value = mkOption {
description = "Value of taint.";
type = str;
};
effect = mkOption {
description = "Effect of taint.";
example = "NoSchedule";
type = enum ["NoSchedule" "PreferNoSchedule" "NoExecute"];
};
};
};
taints = concatMapStringsSep "," (v: "${v.key}=${v.value}:${v.effect}") (mapAttrsToList (n: v: v) cfg.taints);
in
{
imports = [
(mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "applyManifests" ] "")
(mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
(mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "")
];
###### interface
options.services.kubernetes.kubelet = with lib.types; {
address = mkOption {
description = "Kubernetes kubelet info server listening address.";
default = "0.0.0.0";
type = str;
};
clusterDns = mkOption {
description = "Use alternative DNS.";
default = "10.1.0.1";
type = str;
};
clusterDomain = mkOption {
description = "Use alternative domain.";
default = config.services.kubernetes.addons.dns.clusterDomain;
defaultText = literalExpression "config.${options.services.kubernetes.addons.dns.clusterDomain}";
type = str;
};
clientCaFile = mkOption {
description = "Kubernetes apiserver CA file for client authentication.";
default = top.caFile;
defaultText = literalExpression "config.${otop.caFile}";
type = nullOr path;
};
cni = {
packages = mkOption {
description = "List of network plugin packages to install.";
type = listOf package;
default = [];
};
config = mkOption {
description = "Kubernetes CNI configuration.";
type = listOf attrs;
default = [];
example = literalExpression ''
[{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.22.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
} {
"cniVersion": "0.3.1",
"type": "loopback"
}]
'';
};
configDir = mkOption {
description = "Path to Kubernetes CNI configuration directory.";
type = nullOr path;
default = null;
};
};
containerRuntime = mkOption {
description = "Which container runtime type to use";
type = enum ["docker" "remote"];
default = "remote";
};
containerRuntimeEndpoint = mkOption {
description = "Endpoint at which to find the container runtime api interface/socket";
type = str;
default = "unix:///run/containerd/containerd.sock";
};
enable = mkEnableOption "Kubernetes kubelet.";
extraOpts = mkOption {
description = "Kubernetes kubelet extra command line options.";
default = "";
type = separatedString " ";
};
featureGates = mkOption {
description = "List set of feature gates";
default = top.featureGates;
defaultText = literalExpression "config.${otop.featureGates}";
type = listOf str;
};
healthz = {
bind = mkOption {
description = "Kubernetes kubelet healthz listening address.";
default = "127.0.0.1";
type = str;
};
port = mkOption {
description = "Kubernetes kubelet healthz port.";
default = 10248;
type = int;
};
};
hostname = mkOption {
description = "Kubernetes kubelet hostname override.";
default = config.networking.hostName;
defaultText = literalExpression "config.networking.hostName";
type = str;
};
kubeconfig = top.lib.mkKubeConfigOptions "Kubelet";
manifests = mkOption {
description = "List of manifests to bootstrap with kubelet (only pods can be created as manifest entry)";
type = attrsOf attrs;
default = {};
};
networkPlugin = mkOption {
description = "Network plugin to use by Kubernetes.";
type = nullOr (enum ["cni" "kubenet"]);
default = "kubenet";
};
nodeIp = mkOption {
description = "IP address of the node. If set, kubelet will use this IP address for the node.";
default = null;
type = nullOr str;
};
registerNode = mkOption {
description = "Whether to auto register kubelet with API server.";
default = true;
type = bool;
};
port = mkOption {
description = "Kubernetes kubelet info server listening port.";
default = 10250;
type = int;
};
seedDockerImages = mkOption {
description = "List of docker images to preload on system";
default = [];
type = listOf package;
};
taints = mkOption {
description = "Node taints (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/).";
default = {};
type = attrsOf (submodule [ taintOptions ]);
};
tlsCertFile = mkOption {
description = "File containing x509 Certificate for HTTPS.";
default = null;
type = nullOr path;
};
tlsKeyFile = mkOption {
description = "File containing x509 private key matching tlsCertFile.";
default = null;
type = nullOr path;
};
unschedulable = mkOption {
description = "Whether to set node taint to unschedulable=true as it is the case of node that has only master role.";
default = false;
type = bool;
};
verbosity = mkOption {
description = ''
Optional glog verbosity level for logging statements. See
<link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
'';
default = null;
type = nullOr int;
};
};
###### implementation
config = mkMerge [
(mkIf cfg.enable {
environment.etc."cni/net.d".source = cniConfig;
services.kubernetes.kubelet.seedDockerImages = [infraContainer];
boot.kernel.sysctl = {
"net.bridge.bridge-nf-call-iptables" = 1;
"net.ipv4.ip_forward" = 1;
"net.bridge.bridge-nf-call-ip6tables" = 1;
};
systemd.services.kubelet = {
description = "Kubernetes Kubelet Service";
wantedBy = [ "kubernetes.target" ];
after = [ "containerd.service" "network.target" "kube-apiserver.service" ];
path = with pkgs; [
gitMinimal
openssh
util-linux
iproute2
ethtool
thin-provisioning-tools
iptables
socat
] ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package ++ top.path;
preStart = ''
${concatMapStrings (img: ''
echo "Seeding container image: ${img}"
${if (lib.hasSuffix "gz" img) then
''${pkgs.gzip}/bin/zcat "${img}" | ${pkgs.containerd}/bin/ctr -n k8s.io image import --all-platforms -''
else
''${pkgs.coreutils}/bin/cat "${img}" | ${pkgs.containerd}/bin/ctr -n k8s.io image import --all-platforms -''
}
'') cfg.seedDockerImages}
rm /opt/cni/bin/* || true
${concatMapStrings (package: ''
echo "Linking cni package: ${package}"
ln -fs ${package}/bin/* /opt/cni/bin
'') cfg.cni.packages}
'';
serviceConfig = {
Slice = "kubernetes.slice";
CPUAccounting = true;
MemoryAccounting = true;
Restart = "on-failure";
RestartSec = "1000ms";
ExecStart = ''${top.package}/bin/kubelet \
--address=${cfg.address} \
--authentication-token-webhook \
--authentication-token-webhook-cache-ttl="10s" \
--authorization-mode=Webhook \
${optionalString (cfg.clientCaFile != null)
"--client-ca-file=${cfg.clientCaFile}"} \
${optionalString (cfg.clusterDns != "")
"--cluster-dns=${cfg.clusterDns}"} \
${optionalString (cfg.clusterDomain != "")
"--cluster-domain=${cfg.clusterDomain}"} \
--cni-conf-dir=${cniConfig} \
${optionalString (cfg.featureGates != [])
"--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
--hairpin-mode=hairpin-veth \
--healthz-bind-address=${cfg.healthz.bind} \
--healthz-port=${toString cfg.healthz.port} \
--hostname-override=${cfg.hostname} \
--kubeconfig=${kubeconfig} \
${optionalString (cfg.networkPlugin != null)
"--network-plugin=${cfg.networkPlugin}"} \
${optionalString (cfg.nodeIp != null)
"--node-ip=${cfg.nodeIp}"} \
--pod-infra-container-image=pause \
${optionalString (cfg.manifests != {})
"--pod-manifest-path=/etc/${manifestPath}"} \
--port=${toString cfg.port} \
--register-node=${boolToString cfg.registerNode} \
${optionalString (taints != "")
"--register-with-taints=${taints}"} \
--root-dir=${top.dataDir} \
${optionalString (cfg.tlsCertFile != null)
"--tls-cert-file=${cfg.tlsCertFile}"} \
${optionalString (cfg.tlsKeyFile != null)
"--tls-private-key-file=${cfg.tlsKeyFile}"} \
${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
--container-runtime=${cfg.containerRuntime} \
--container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \
--cgroup-driver=systemd \
${cfg.extraOpts}
'';
WorkingDirectory = top.dataDir;
};
unitConfig = {
StartLimitIntervalSec = 0;
};
};
# Allways include cni plugins
services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins pkgs.cni-plugin-flannel];
boot.kernelModules = ["br_netfilter" "overlay"];
services.kubernetes.kubelet.hostname = with config.networking;
mkDefault (hostName + optionalString (domain != null) ".${domain}");
services.kubernetes.pki.certs = with top.lib; {
kubelet = mkCert {
name = "kubelet";
CN = top.kubelet.hostname;
action = "systemctl restart kubelet.service";
};
kubeletClient = mkCert {
name = "kubelet-client";
CN = "system:node:${top.kubelet.hostname}";
fields = {
O = "system:nodes";
};
action = "systemctl restart kubelet.service";
};
};
services.kubernetes.kubelet.kubeconfig.server = mkDefault top.apiserverAddress;
})
(mkIf (cfg.enable && cfg.manifests != {}) {
environment.etc = mapAttrs' (name: manifest:
nameValuePair "${manifestPath}/${name}.json" {
text = builtins.toJSON manifest;
mode = "0755";
}
) cfg.manifests;
})
(mkIf (cfg.unschedulable && cfg.enable) {
services.kubernetes.kubelet.taints.unschedulable = {
value = "true";
effect = "NoSchedule";
};
})
];
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,406 @@
{ config, lib, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
cfg = top.pki;
csrCA = pkgs.writeText "kube-pki-cacert-csr.json" (builtins.toJSON {
key = {
algo = "rsa";
size = 2048;
};
names = singleton cfg.caSpec;
});
csrCfssl = pkgs.writeText "kube-pki-cfssl-csr.json" (builtins.toJSON {
key = {
algo = "rsa";
size = 2048;
};
CN = top.masterAddress;
hosts = [top.masterAddress] ++ cfg.cfsslAPIExtraSANs;
});
cfsslAPITokenBaseName = "apitoken.secret";
cfsslAPITokenPath = "${config.services.cfssl.dataDir}/${cfsslAPITokenBaseName}";
certmgrAPITokenPath = "${top.secretsPath}/${cfsslAPITokenBaseName}";
cfsslAPITokenLength = 32;
clusterAdminKubeconfig = with cfg.certs.clusterAdmin;
top.lib.mkKubeConfig "cluster-admin" {
server = top.apiserverAddress;
certFile = cert;
keyFile = key;
};
remote = with config.services; "https://${kubernetes.masterAddress}:${toString cfssl.port}";
in
{
###### interface
options.services.kubernetes.pki = with lib.types; {
enable = mkEnableOption "easyCert issuer service";
certs = mkOption {
description = "List of certificate specs to feed to cert generator.";
default = {};
type = attrs;
};
genCfsslCACert = mkOption {
description = ''
Whether to automatically generate cfssl CA certificate and key,
if they don't exist.
'';
default = true;
type = bool;
};
genCfsslAPICerts = mkOption {
description = ''
Whether to automatically generate cfssl API webserver TLS cert and key,
if they don't exist.
'';
default = true;
type = bool;
};
cfsslAPIExtraSANs = mkOption {
description = ''
Extra x509 Subject Alternative Names to be added to the cfssl API webserver TLS cert.
'';
default = [];
example = [ "subdomain.example.com" ];
type = listOf str;
};
genCfsslAPIToken = mkOption {
description = ''
Whether to automatically generate cfssl API-token secret,
if they doesn't exist.
'';
default = true;
type = bool;
};
pkiTrustOnBootstrap = mkOption {
description = "Whether to always trust remote cfssl server upon initial PKI bootstrap.";
default = true;
type = bool;
};
caCertPathPrefix = mkOption {
description = ''
Path-prefrix for the CA-certificate to be used for cfssl signing.
Suffixes ".pem" and "-key.pem" will be automatically appended for
the public and private keys respectively.
'';
default = "${config.services.cfssl.dataDir}/ca";
defaultText = literalExpression ''"''${config.services.cfssl.dataDir}/ca"'';
type = str;
};
caSpec = mkOption {
description = "Certificate specification for the auto-generated CAcert.";
default = {
CN = "kubernetes-cluster-ca";
O = "NixOS";
OU = "services.kubernetes.pki.caSpec";
L = "auto-generated";
};
type = attrs;
};
etcClusterAdminKubeconfig = mkOption {
description = ''
Symlink a kubeconfig with cluster-admin privileges to environment path
(/etc/&lt;path&gt;).
'';
default = null;
type = nullOr str;
};
};
###### implementation
config = mkIf cfg.enable
(let
cfsslCertPathPrefix = "${config.services.cfssl.dataDir}/cfssl";
cfsslCert = "${cfsslCertPathPrefix}.pem";
cfsslKey = "${cfsslCertPathPrefix}-key.pem";
in
{
services.cfssl = mkIf (top.apiserver.enable) {
enable = true;
address = "0.0.0.0";
tlsCert = cfsslCert;
tlsKey = cfsslKey;
configFile = toString (pkgs.writeText "cfssl-config.json" (builtins.toJSON {
signing = {
profiles = {
default = {
usages = ["digital signature"];
auth_key = "default";
expiry = "720h";
};
};
};
auth_keys = {
default = {
type = "standard";
key = "file:${cfsslAPITokenPath}";
};
};
}));
};
systemd.services.cfssl.preStart = with pkgs; with config.services.cfssl; mkIf (top.apiserver.enable)
(concatStringsSep "\n" [
"set -e"
(optionalString cfg.genCfsslCACert ''
if [ ! -f "${cfg.caCertPathPrefix}.pem" ]; then
${cfssl}/bin/cfssl genkey -initca ${csrCA} | \
${cfssl}/bin/cfssljson -bare ${cfg.caCertPathPrefix}
fi
'')
(optionalString cfg.genCfsslAPICerts ''
if [ ! -f "${dataDir}/cfssl.pem" ]; then
${cfssl}/bin/cfssl gencert -ca "${cfg.caCertPathPrefix}.pem" -ca-key "${cfg.caCertPathPrefix}-key.pem" ${csrCfssl} | \
${cfssl}/bin/cfssljson -bare ${cfsslCertPathPrefix}
fi
'')
(optionalString cfg.genCfsslAPIToken ''
if [ ! -f "${cfsslAPITokenPath}" ]; then
head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ' >"${cfsslAPITokenPath}"
fi
chown cfssl "${cfsslAPITokenPath}" && chmod 400 "${cfsslAPITokenPath}"
'')]);
systemd.services.kube-certmgr-bootstrap = {
description = "Kubernetes certmgr bootstrapper";
wantedBy = [ "certmgr.service" ];
after = [ "cfssl.target" ];
script = concatStringsSep "\n" [''
set -e
# If there's a cfssl (cert issuer) running locally, then don't rely on user to
# manually paste it in place. Just symlink.
# otherwise, create the target file, ready for users to insert the token
mkdir -p "$(dirname "${certmgrAPITokenPath}")"
if [ -f "${cfsslAPITokenPath}" ]; then
ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}"
else
touch "${certmgrAPITokenPath}" && chmod 600 "${certmgrAPITokenPath}"
fi
''
(optionalString (cfg.pkiTrustOnBootstrap) ''
if [ ! -f "${top.caFile}" ] || [ $(cat "${top.caFile}" | wc -c) -lt 1 ]; then
${pkgs.curl}/bin/curl --fail-early -f -kd '{}' ${remote}/api/v1/cfssl/info | \
${pkgs.cfssl}/bin/cfssljson -stdout >${top.caFile}
fi
'')
];
serviceConfig = {
RestartSec = "10s";
Restart = "on-failure";
};
};
services.certmgr = {
enable = true;
package = pkgs.certmgr-selfsigned;
svcManager = "command";
specs =
let
mkSpec = _: cert: {
inherit (cert) action;
authority = {
inherit remote;
file.path = cert.caCert;
root_ca = cert.caCert;
profile = "default";
auth_key_file = certmgrAPITokenPath;
};
certificate = {
path = cert.cert;
};
private_key = cert.privateKeyOptions;
request = {
hosts = [cert.CN] ++ cert.hosts;
inherit (cert) CN;
key = {
algo = "rsa";
size = 2048;
};
names = [ cert.fields ];
};
};
in
mapAttrs mkSpec cfg.certs;
};
#TODO: Get rid of kube-addon-manager in the future for the following reasons
# - it is basically just a shell script wrapped around kubectl
# - it assumes that it is clusterAdmin or can gain clusterAdmin rights through serviceAccount
# - it is designed to be used with k8s system components only
# - it would be better with a more Nix-oriented way of managing addons
systemd.services.kube-addon-manager = mkIf top.addonManager.enable (mkMerge [{
environment.KUBECONFIG = with cfg.certs.addonManager;
top.lib.mkKubeConfig "addon-manager" {
server = top.apiserverAddress;
certFile = cert;
keyFile = key;
};
}
(optionalAttrs (top.addonManager.bootstrapAddons != {}) {
serviceConfig.PermissionsStartOnly = true;
preStart = with pkgs;
let
files = mapAttrsToList (n: v: writeText "${n}.json" (builtins.toJSON v))
top.addonManager.bootstrapAddons;
in
''
export KUBECONFIG=${clusterAdminKubeconfig}
${kubernetes}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files}
'';
})]);
environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (!isNull cfg.etcClusterAdminKubeconfig)
clusterAdminKubeconfig;
environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [
(pkgs.writeScriptBin "nixos-kubernetes-node-join" ''
set -e
exec 1>&2
if [ $# -gt 0 ]; then
echo "Usage: $(basename $0)"
echo ""
echo "No args. Apitoken must be provided on stdin."
echo "To get the apitoken, execute: 'sudo cat ${certmgrAPITokenPath}' on the master node."
exit 1
fi
if [ $(id -u) != 0 ]; then
echo "Run as root please."
exit 1
fi
read -r token
if [ ''${#token} != ${toString cfsslAPITokenLength} ]; then
echo "Token must be of length ${toString cfsslAPITokenLength}."
exit 1
fi
echo $token > ${certmgrAPITokenPath}
chmod 600 ${certmgrAPITokenPath}
echo "Restarting certmgr..." >&1
systemctl restart certmgr
echo "Waiting for certs to appear..." >&1
${optionalString top.kubelet.enable ''
while [ ! -f ${cfg.certs.kubelet.cert} ]; do sleep 1; done
echo "Restarting kubelet..." >&1
systemctl restart kubelet
''}
${optionalString top.proxy.enable ''
while [ ! -f ${cfg.certs.kubeProxyClient.cert} ]; do sleep 1; done
echo "Restarting kube-proxy..." >&1
systemctl restart kube-proxy
''}
${optionalString top.flannel.enable ''
while [ ! -f ${cfg.certs.flannelClient.cert} ]; do sleep 1; done
echo "Restarting flannel..." >&1
systemctl restart flannel
''}
echo "Node joined succesfully"
'')];
# isolate etcd on loopback at the master node
# easyCerts doesn't support multimaster clusters anyway atm.
services.etcd = with cfg.certs.etcd; {
listenClientUrls = ["https://127.0.0.1:2379"];
listenPeerUrls = ["https://127.0.0.1:2380"];
advertiseClientUrls = ["https://etcd.local:2379"];
initialCluster = ["${top.masterAddress}=https://etcd.local:2380"];
initialAdvertisePeerUrls = ["https://etcd.local:2380"];
certFile = mkDefault cert;
keyFile = mkDefault key;
trustedCaFile = mkDefault caCert;
};
networking.extraHosts = mkIf (config.services.etcd.enable) ''
127.0.0.1 etcd.${top.addons.dns.clusterDomain} etcd.local
'';
services.flannel = with cfg.certs.flannelClient; {
kubeconfig = top.lib.mkKubeConfig "flannel" {
server = top.apiserverAddress;
certFile = cert;
keyFile = key;
};
};
services.kubernetes = {
apiserver = mkIf top.apiserver.enable (with cfg.certs.apiServer; {
etcd = with cfg.certs.apiserverEtcdClient; {
servers = ["https://etcd.local:2379"];
certFile = mkDefault cert;
keyFile = mkDefault key;
caFile = mkDefault caCert;
};
clientCaFile = mkDefault caCert;
tlsCertFile = mkDefault cert;
tlsKeyFile = mkDefault key;
serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.cert;
serviceAccountSigningKeyFile = mkDefault cfg.certs.serviceAccount.key;
kubeletClientCaFile = mkDefault caCert;
kubeletClientCertFile = mkDefault cfg.certs.apiserverKubeletClient.cert;
kubeletClientKeyFile = mkDefault cfg.certs.apiserverKubeletClient.key;
proxyClientCertFile = mkDefault cfg.certs.apiserverProxyClient.cert;
proxyClientKeyFile = mkDefault cfg.certs.apiserverProxyClient.key;
});
controllerManager = mkIf top.controllerManager.enable {
serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.key;
rootCaFile = cfg.certs.controllerManagerClient.caCert;
kubeconfig = with cfg.certs.controllerManagerClient; {
certFile = mkDefault cert;
keyFile = mkDefault key;
};
};
scheduler = mkIf top.scheduler.enable {
kubeconfig = with cfg.certs.schedulerClient; {
certFile = mkDefault cert;
keyFile = mkDefault key;
};
};
kubelet = mkIf top.kubelet.enable {
clientCaFile = mkDefault cfg.certs.kubelet.caCert;
tlsCertFile = mkDefault cfg.certs.kubelet.cert;
tlsKeyFile = mkDefault cfg.certs.kubelet.key;
kubeconfig = with cfg.certs.kubeletClient; {
certFile = mkDefault cert;
keyFile = mkDefault key;
};
};
proxy = mkIf top.proxy.enable {
kubeconfig = with cfg.certs.kubeProxyClient; {
certFile = mkDefault cert;
keyFile = mkDefault key;
};
};
};
});
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,102 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
otop = options.services.kubernetes;
cfg = top.proxy;
in
{
imports = [
(mkRenamedOptionModule [ "services" "kubernetes" "proxy" "address" ] ["services" "kubernetes" "proxy" "bindAddress"])
];
###### interface
options.services.kubernetes.proxy = with lib.types; {
bindAddress = mkOption {
description = "Kubernetes proxy listening address.";
default = "0.0.0.0";
type = str;
};
enable = mkEnableOption "Kubernetes proxy";
extraOpts = mkOption {
description = "Kubernetes proxy extra command line options.";
default = "";
type = separatedString " ";
};
featureGates = mkOption {
description = "List set of feature gates";
default = top.featureGates;
defaultText = literalExpression "config.${otop.featureGates}";
type = listOf str;
};
hostname = mkOption {
description = "Kubernetes proxy hostname override.";
default = config.networking.hostName;
defaultText = literalExpression "config.networking.hostName";
type = str;
};
kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes proxy";
verbosity = mkOption {
description = ''
Optional glog verbosity level for logging statements. See
<link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
'';
default = null;
type = nullOr int;
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.kube-proxy = {
description = "Kubernetes Proxy Service";
wantedBy = [ "kubernetes.target" ];
after = [ "kube-apiserver.service" ];
path = with pkgs; [ iptables conntrack-tools ];
serviceConfig = {
Slice = "kubernetes.slice";
ExecStart = ''${top.package}/bin/kube-proxy \
--bind-address=${cfg.bindAddress} \
${optionalString (top.clusterCidr!=null)
"--cluster-cidr=${top.clusterCidr}"} \
${optionalString (cfg.featureGates != [])
"--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
--hostname-override=${cfg.hostname} \
--kubeconfig=${top.lib.mkKubeConfig "kube-proxy" cfg.kubeconfig} \
${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
${cfg.extraOpts}
'';
WorkingDirectory = top.dataDir;
Restart = "on-failure";
RestartSec = 5;
};
unitConfig = {
StartLimitIntervalSec = 0;
};
};
services.kubernetes.proxy.hostname = with config.networking; mkDefault hostName;
services.kubernetes.pki.certs = {
kubeProxyClient = top.lib.mkCert {
name = "kube-proxy-client";
CN = "system:kube-proxy";
action = "systemctl restart kube-proxy.service";
};
};
services.kubernetes.proxy.kubeconfig.server = mkDefault top.apiserverAddress;
};
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,101 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
top = config.services.kubernetes;
otop = options.services.kubernetes;
cfg = top.scheduler;
in
{
###### interface
options.services.kubernetes.scheduler = with lib.types; {
address = mkOption {
description = "Kubernetes scheduler listening address.";
default = "127.0.0.1";
type = str;
};
enable = mkEnableOption "Kubernetes scheduler";
extraOpts = mkOption {
description = "Kubernetes scheduler extra command line options.";
default = "";
type = separatedString " ";
};
featureGates = mkOption {
description = "List set of feature gates";
default = top.featureGates;
defaultText = literalExpression "config.${otop.featureGates}";
type = listOf str;
};
kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes scheduler";
leaderElect = mkOption {
description = "Whether to start leader election before executing main loop.";
type = bool;
default = true;
};
port = mkOption {
description = "Kubernetes scheduler listening port.";
default = 10251;
type = int;
};
verbosity = mkOption {
description = ''
Optional glog verbosity level for logging statements. See
<link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
'';
default = null;
type = nullOr int;
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.kube-scheduler = {
description = "Kubernetes Scheduler Service";
wantedBy = [ "kubernetes.target" ];
after = [ "kube-apiserver.service" ];
serviceConfig = {
Slice = "kubernetes.slice";
ExecStart = ''${top.package}/bin/kube-scheduler \
--bind-address=${cfg.address} \
${optionalString (cfg.featureGates != [])
"--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
--kubeconfig=${top.lib.mkKubeConfig "kube-scheduler" cfg.kubeconfig} \
--leader-elect=${boolToString cfg.leaderElect} \
--secure-port=${toString cfg.port} \
${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
${cfg.extraOpts}
'';
WorkingDirectory = top.dataDir;
User = "kubernetes";
Group = "kubernetes";
Restart = "on-failure";
RestartSec = 5;
};
unitConfig = {
StartLimitIntervalSec = 0;
};
};
services.kubernetes.pki.certs = {
schedulerClient = top.lib.mkCert {
name = "kube-scheduler-client";
CN = "system:kube-scheduler";
action = "systemctl restart kube-scheduler.service";
};
};
services.kubernetes.scheduler.kubeconfig.server = mkDefault top.apiserverAddress;
};
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.pacemaker;
in
{
# interface
options.services.pacemaker = {
enable = mkEnableOption "pacemaker";
package = mkOption {
type = types.package;
default = pkgs.pacemaker;
defaultText = literalExpression "pkgs.pacemaker";
description = "Package that should be used for pacemaker.";
};
};
# implementation
config = mkIf cfg.enable {
assertions = [ {
assertion = config.services.corosync.enable;
message = ''
Enabling services.pacemaker requires a services.corosync configuration.
'';
} ];
environment.systemPackages = [ cfg.package ];
# required by pacemaker
users.users.hacluster = {
isSystemUser = true;
group = "pacemaker";
home = "/var/lib/pacemaker";
};
users.groups.pacemaker = {};
systemd.tmpfiles.rules = [
"d /var/log/pacemaker 0700 hacluster pacemaker -"
];
systemd.packages = [ cfg.package ];
systemd.services.pacemaker = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "pacemaker";
StateDirectoryMode = "0700";
};
};
};
}

View file

@ -0,0 +1,162 @@
{config, pkgs, lib, ...}:
let
cfg = config.services.spark;
in
with lib;
{
options = {
services.spark = {
master = {
enable = mkEnableOption "Spark master service";
bind = mkOption {
type = types.str;
description = "Address the spark master binds to.";
default = "127.0.0.1";
example = "0.0.0.0";
};
restartIfChanged = mkOption {
type = types.bool;
description = ''
Automatically restart master service on config change.
This can be set to false to defer restarts on clusters running critical applications.
Please consider the security implications of inadvertently running an older version,
and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
'';
default = true;
};
extraEnvironment = mkOption {
type = types.attrsOf types.str;
description = "Extra environment variables to pass to spark master. See spark-standalone documentation.";
default = {};
example = {
SPARK_MASTER_WEBUI_PORT = 8181;
SPARK_MASTER_OPTS = "-Dspark.deploy.defaultCores=5";
};
};
};
worker = {
enable = mkEnableOption "Spark worker service";
workDir = mkOption {
type = types.path;
description = "Spark worker work dir.";
default = "/var/lib/spark";
};
master = mkOption {
type = types.str;
description = "Address of the spark master.";
default = "127.0.0.1:7077";
};
restartIfChanged = mkOption {
type = types.bool;
description = ''
Automatically restart worker service on config change.
This can be set to false to defer restarts on clusters running critical applications.
Please consider the security implications of inadvertently running an older version,
and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
'';
default = true;
};
extraEnvironment = mkOption {
type = types.attrsOf types.str;
description = "Extra environment variables to pass to spark worker.";
default = {};
example = {
SPARK_WORKER_CORES = 5;
SPARK_WORKER_MEMORY = "2g";
};
};
};
confDir = mkOption {
type = types.path;
description = "Spark configuration directory. Spark will use the configuration files (spark-defaults.conf, spark-env.sh, log4j.properties, etc) from this directory.";
default = "${cfg.package}/lib/${cfg.package.untarDir}/conf";
defaultText = literalExpression ''"''${package}/lib/''${package.untarDir}/conf"'';
};
logDir = mkOption {
type = types.path;
description = "Spark log directory.";
default = "/var/log/spark";
};
package = mkOption {
type = types.package;
description = "Spark package.";
default = pkgs.spark;
defaultText = literalExpression "pkgs.spark";
example = literalExpression ''pkgs.spark.overrideAttrs (super: rec {
pname = "spark";
version = "2.4.4";
src = pkgs.fetchzip {
url = "mirror://apache/spark/"''${pname}-''${version}/''${pname}-''${version}-bin-without-hadoop.tgz";
sha256 = "1a9w5k0207fysgpxx6db3a00fs5hdc2ncx99x4ccy2s0v5ndc66g";
};
})'';
};
};
};
config = lib.mkIf (cfg.worker.enable || cfg.master.enable) {
environment.systemPackages = [ cfg.package ];
systemd = {
services = {
spark-master = lib.mkIf cfg.master.enable {
path = with pkgs; [ procps openssh nettools ];
description = "spark master service.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartIfChanged = cfg.master.restartIfChanged;
environment = cfg.master.extraEnvironment // {
SPARK_MASTER_HOST = cfg.master.bind;
SPARK_CONF_DIR = cfg.confDir;
SPARK_LOG_DIR = cfg.logDir;
};
serviceConfig = {
Type = "forking";
User = "spark";
Group = "spark";
WorkingDirectory = "${cfg.package}/lib/${cfg.package.untarDir}";
ExecStart = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/start-master.sh";
ExecStop = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/stop-master.sh";
TimeoutSec = 300;
StartLimitBurst=10;
Restart = "always";
};
};
spark-worker = lib.mkIf cfg.worker.enable {
path = with pkgs; [ procps openssh nettools rsync ];
description = "spark master service.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartIfChanged = cfg.worker.restartIfChanged;
environment = cfg.worker.extraEnvironment // {
SPARK_MASTER = cfg.worker.master;
SPARK_CONF_DIR = cfg.confDir;
SPARK_LOG_DIR = cfg.logDir;
SPARK_WORKER_DIR = cfg.worker.workDir;
};
serviceConfig = {
Type = "forking";
User = "spark";
WorkingDirectory = "${cfg.package}/lib/${cfg.package.untarDir}";
ExecStart = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/start-worker.sh spark://${cfg.worker.master}";
ExecStop = "${cfg.package}/lib/${cfg.package.untarDir}/sbin/stop-worker.sh";
TimeoutSec = 300;
StartLimitBurst=10;
Restart = "always";
};
};
};
tmpfiles.rules = [
"d '${cfg.worker.workDir}' - spark spark - -"
"d '${cfg.logDir}' - spark spark - -"
];
};
users = {
users.spark = {
description = "spark user.";
group = "spark";
isSystemUser = true;
};
groups.spark = { };
};
};
}

View file

@ -0,0 +1,131 @@
{config, lib, pkgs, ...}:
with lib;
let
cfg = config.services.boinc;
allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc";
fhsEnv = pkgs.buildFHSUserEnv {
name = "boinc-fhs-env";
targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages;
runScript = "/bin/boinc_client";
};
fhsEnvExecutable = "${fhsEnv}/bin/${fhsEnv.name}";
in
{
options.services.boinc = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the BOINC distributed computing client. If this
option is set to true, the boinc_client daemon will be run as a
background service. The boinccmd command can be used to control the
daemon.
'';
};
package = mkOption {
type = types.package;
default = pkgs.boinc;
defaultText = literalExpression "pkgs.boinc";
description = ''
Which BOINC package to use.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/boinc";
description = ''
The directory in which to store BOINC's configuration and data files.
'';
};
allowRemoteGuiRpc = mkOption {
type = types.bool;
default = false;
description = ''
If set to true, any remote host can connect to and control this BOINC
client (subject to password authentication). If instead set to false,
only the hosts listed in <varname>dataDir</varname>/remote_hosts.cfg will be allowed to
connect.
See also: <link xlink:href="http://boinc.berkeley.edu/wiki/Controlling_BOINC_remotely#Remote_access"/>
'';
};
extraEnvPackages = mkOption {
type = types.listOf types.package;
default = [];
example = literalExpression "[ pkgs.virtualbox ]";
description = ''
Additional packages to make available in the environment in which
BOINC will run. Common choices are:
<variablelist>
<varlistentry>
<term><varname>pkgs.virtualbox</varname></term>
<listitem><para>
The VirtualBox virtual machine framework. Required by some BOINC
projects, such as ATLAS@home.
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>pkgs.ocl-icd</varname></term>
<listitem><para>
OpenCL infrastructure library. Required by BOINC projects that
use OpenCL, in addition to a device-specific OpenCL driver.
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>pkgs.linuxPackages.nvidia_x11</varname></term>
<listitem><para>
Provides CUDA libraries. Required by BOINC projects that use
CUDA. Note that this requires an NVIDIA graphics device to be
present on the system.
</para><para>
Also provides OpenCL drivers for NVIDIA GPUs;
<varname>pkgs.ocl-icd</varname> is also needed in this case.
</para></listitem>
</varlistentry>
</variablelist>
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = [cfg.package];
users.users.boinc = {
group = "boinc";
createHome = false;
description = "BOINC Client";
home = cfg.dataDir;
isSystemUser = true;
};
users.groups.boinc = {};
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - boinc boinc - -"
];
systemd.services.boinc = {
description = "BOINC Client";
after = ["network.target"];
wantedBy = ["multi-user.target"];
script = ''
${fhsEnvExecutable} --dir ${cfg.dataDir} ${allowRemoteGuiRpcFlag}
'';
serviceConfig = {
User = "boinc";
Nice = 10;
};
};
};
meta = {
maintainers = with lib.maintainers; [kierdavis];
};
}

View file

@ -0,0 +1,91 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.foldingathome;
args =
["--team" "${toString cfg.team}"]
++ lib.optionals (cfg.user != null) ["--user" cfg.user]
++ cfg.extraArgs
;
in
{
imports = [
(mkRenamedOptionModule [ "services" "foldingAtHome" ] [ "services" "foldingathome" ])
(mkRenamedOptionModule [ "services" "foldingathome" "nickname" ] [ "services" "foldingathome" "user" ])
(mkRemovedOptionModule [ "services" "foldingathome" "config" ] ''
Use <literal>services.foldingathome.extraArgs instead<literal>
'')
];
options.services.foldingathome = {
enable = mkEnableOption "Enable the Folding@home client";
package = mkOption {
type = types.package;
default = pkgs.fahclient;
defaultText = literalExpression "pkgs.fahclient";
description = ''
Which Folding@home client to use.
'';
};
user = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The user associated with the reported computation results. This will
be used in the ranking statistics.
'';
};
team = mkOption {
type = types.int;
default = 236565;
description = ''
The team ID associated with the reported computation results. This
will be used in the ranking statistics.
By default, use the NixOS folding@home team ID is being used.
'';
};
daemonNiceLevel = mkOption {
type = types.ints.between (-20) 19;
default = 0;
description = ''
Daemon process priority for FAHClient.
0 is the default Unix process priority, 19 is the lowest.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Extra startup options for the FAHClient. Run
<literal>FAHClient --help</literal> to find all the available options.
'';
};
};
config = mkIf cfg.enable {
systemd.services.foldingathome = {
description = "Folding@home client";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
script = ''
exec ${cfg.package}/bin/FAHClient ${lib.escapeShellArgs args}
'';
serviceConfig = {
DynamicUser = true;
StateDirectory = "foldingathome";
Nice = cfg.daemonNiceLevel;
WorkingDirectory = "%S/foldingathome";
};
};
};
meta = {
maintainers = with lib.maintainers; [ zimbatm ];
};
}

View file

@ -0,0 +1,443 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.slurm;
opt = options.services.slurm;
# configuration file can be generated by http://slurm.schedmd.com/configurator.html
defaultUser = "slurm";
configFile = pkgs.writeTextDir "slurm.conf"
''
ClusterName=${cfg.clusterName}
StateSaveLocation=${cfg.stateSaveLocation}
SlurmUser=${cfg.user}
${optionalString (cfg.controlMachine != null) "controlMachine=${cfg.controlMachine}"}
${optionalString (cfg.controlAddr != null) "controlAddr=${cfg.controlAddr}"}
${toString (map (x: "NodeName=${x}\n") cfg.nodeName)}
${toString (map (x: "PartitionName=${x}\n") cfg.partitionName)}
PlugStackConfig=${plugStackConfig}/plugstack.conf
ProctrackType=${cfg.procTrackType}
${cfg.extraConfig}
'';
plugStackConfig = pkgs.writeTextDir "plugstack.conf"
''
${optionalString cfg.enableSrunX11 "optional ${pkgs.slurm-spank-x11}/lib/x11.so"}
${cfg.extraPlugstackConfig}
'';
cgroupConfig = pkgs.writeTextDir "cgroup.conf"
''
${cfg.extraCgroupConfig}
'';
slurmdbdConf = pkgs.writeText "slurmdbd.conf"
''
DbdHost=${cfg.dbdserver.dbdHost}
SlurmUser=${cfg.user}
StorageType=accounting_storage/mysql
StorageUser=${cfg.dbdserver.storageUser}
${cfg.dbdserver.extraConfig}
'';
# slurm expects some additional config files to be
# in the same directory as slurm.conf
etcSlurm = pkgs.symlinkJoin {
name = "etc-slurm";
paths = [ configFile cgroupConfig plugStackConfig ] ++ cfg.extraConfigPaths;
};
in
{
###### interface
meta.maintainers = [ maintainers.markuskowa ];
options = {
services.slurm = {
server = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the slurm control daemon.
Note that the standard authentication method is "munge".
The "munge" service needs to be provided with a password file in order for
slurm to work properly (see <literal>services.munge.password</literal>).
'';
};
};
dbdserver = {
enable = mkEnableOption "SlurmDBD service";
dbdHost = mkOption {
type = types.str;
default = config.networking.hostName;
defaultText = literalExpression "config.networking.hostName";
description = ''
Hostname of the machine where <literal>slurmdbd</literal>
is running (i.e. name returned by <literal>hostname -s</literal>).
'';
};
storageUser = mkOption {
type = types.str;
default = cfg.user;
defaultText = literalExpression "config.${opt.user}";
description = ''
Database user name.
'';
};
storagePassFile = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Path to file with database password. The content of this will be used to
create the password for the <literal>StoragePass</literal> option.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration for <literal>slurmdbd.conf</literal> See also:
<citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
<manvolnum>8</manvolnum></citerefentry>.
'';
};
};
client = {
enable = mkEnableOption "slurm client daemon";
};
enableStools = mkOption {
type = types.bool;
default = false;
description = ''
Whether to provide a slurm.conf file.
Enable this option if you do not run a slurm daemon on this host
(i.e. <literal>server.enable</literal> and <literal>client.enable</literal> are <literal>false</literal>)
but you still want to run slurm commands from this host.
'';
};
package = mkOption {
type = types.package;
default = pkgs.slurm.override { enableX11 = ! cfg.enableSrunX11; };
defaultText = literalExpression "pkgs.slurm";
example = literalExpression "pkgs.slurm-full";
description = ''
The package to use for slurm binaries.
'';
};
controlMachine = mkOption {
type = types.nullOr types.str;
default = null;
example = null;
description = ''
The short hostname of the machine where SLURM control functions are
executed (i.e. the name returned by the command "hostname -s", use "tux001"
rather than "tux001.my.com").
'';
};
controlAddr = mkOption {
type = types.nullOr types.str;
default = cfg.controlMachine;
defaultText = literalExpression "config.${opt.controlMachine}";
example = null;
description = ''
Name that ControlMachine should be referred to in establishing a
communications path.
'';
};
clusterName = mkOption {
type = types.str;
default = "default";
example = "myCluster";
description = ''
Necessary to distinguish accounting records in a multi-cluster environment.
'';
};
nodeName = mkOption {
type = types.listOf types.str;
default = [];
example = literalExpression ''[ "linux[1-32] CPUs=1 State=UNKNOWN" ];'';
description = ''
Name that SLURM uses to refer to a node (or base partition for BlueGene
systems). Typically this would be the string that "/bin/hostname -s"
returns. Note that now you have to write node's parameters after the name.
'';
};
partitionName = mkOption {
type = types.listOf types.str;
default = [];
example = literalExpression ''[ "debug Nodes=linux[1-32] Default=YES MaxTime=INFINITE State=UP" ];'';
description = ''
Name by which the partition may be referenced. Note that now you have
to write the partition's parameters after the name.
'';
};
enableSrunX11 = mkOption {
default = false;
type = types.bool;
description = ''
If enabled srun will accept the option "--x11" to allow for X11 forwarding
from within an interactive session or a batch job. This activates the
slurm-spank-x11 module. Note that this option also enables
<option>services.openssh.forwardX11</option> on the client.
This option requires slurm to be compiled without native X11 support.
The default behavior is to re-compile the slurm package with native X11
support disabled if this option is set to true.
To use the native X11 support add <literal>PrologFlags=X11</literal> in <option>extraConfig</option>.
Note that this method will only work RSA SSH host keys.
'';
};
procTrackType = mkOption {
type = types.str;
default = "proctrack/linuxproc";
description = ''
Plugin to be used for process tracking on a job step basis.
The slurmd daemon uses this mechanism to identify all processes
which are children of processes it spawns for a user job step.
'';
};
stateSaveLocation = mkOption {
type = types.str;
default = "/var/spool/slurmctld";
description = ''
Directory into which the Slurm controller, slurmctld, saves its state.
'';
};
user = mkOption {
type = types.str;
default = defaultUser;
description = ''
Set this option when you want to run the slurmctld daemon
as something else than the default slurm user "slurm".
Note that the UID of this user needs to be the same
on all nodes.
'';
};
extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration options that will be added verbatim at
the end of the slurm configuration file.
'';
};
extraPlugstackConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration that will be added to the end of <literal>plugstack.conf</literal>.
'';
};
extraCgroupConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra configuration for <literal>cgroup.conf</literal>. This file is
used when <literal>procTrackType=proctrack/cgroup</literal>.
'';
};
extraConfigPaths = mkOption {
type = with types; listOf path;
default = [];
description = ''
Slurm expects config files for plugins in the same path
as <literal>slurm.conf</literal>. Add extra nix store
paths that should be merged into same directory as
<literal>slurm.conf</literal>.
'';
};
etcSlurm = mkOption {
type = types.path;
internal = true;
default = etcSlurm;
defaultText = literalDocBook ''
Directory created from generated config files and
<literal>config.${opt.extraConfigPaths}</literal>.
'';
description = ''
Path to directory with slurm config files. This option is set by default from the
Slurm module and is meant to make the Slurm config file available to other modules.
'';
};
};
};
imports = [
(mkRemovedOptionModule [ "services" "slurm" "dbdserver" "storagePass" ] ''
This option has been removed so that the database password is not exposed via the nix store.
Use services.slurm.dbdserver.storagePassFile to provide the database password.
'')
(mkRemovedOptionModule [ "services" "slurm" "dbdserver" "configFile" ] ''
This option has been removed. Use services.slurm.dbdserver.storagePassFile
and services.slurm.dbdserver.extraConfig instead.
'')
];
###### implementation
config =
let
wrappedSlurm = pkgs.stdenv.mkDerivation {
name = "wrappedSlurm";
builder = pkgs.writeText "builder.sh" ''
source $stdenv/setup
mkdir -p $out/bin
find ${getBin cfg.package}/bin -type f -executable | while read EXE
do
exename="$(basename $EXE)"
wrappername="$out/bin/$exename"
cat > "$wrappername" <<EOT
#!/bin/sh
if [ -z "$SLURM_CONF" ]
then
SLURM_CONF="${cfg.etcSlurm}/slurm.conf" "$EXE" "\$@"
else
"$EXE" "\$0"
fi
EOT
chmod +x "$wrappername"
done
mkdir -p $out/share
ln -s ${getBin cfg.package}/share/man $out/share/man
'';
};
in mkIf ( cfg.enableStools ||
cfg.client.enable ||
cfg.server.enable ||
cfg.dbdserver.enable ) {
environment.systemPackages = [ wrappedSlurm ];
services.munge.enable = mkDefault true;
# use a static uid as default to ensure it is the same on all nodes
users.users.slurm = mkIf (cfg.user == defaultUser) {
name = defaultUser;
group = "slurm";
uid = config.ids.uids.slurm;
};
users.groups.slurm.gid = config.ids.uids.slurm;
systemd.services.slurmd = mkIf (cfg.client.enable) {
path = with pkgs; [ wrappedSlurm coreutils ]
++ lib.optional cfg.enableSrunX11 slurm-spank-x11;
wantedBy = [ "multi-user.target" ];
after = [
"systemd-tmpfiles-clean.service"
"munge.service"
"network-online.target"
"remote-fs.target"
];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "forking";
KillMode = "process";
ExecStart = "${wrappedSlurm}/bin/slurmd";
PIDFile = "/run/slurmd.pid";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
LimitMEMLOCK = "infinity";
Delegate="Yes";
};
};
systemd.tmpfiles.rules = mkIf cfg.client.enable [
"d /var/spool/slurmd 755 root root -"
];
services.openssh.forwardX11 = mkIf cfg.client.enable (mkDefault true);
systemd.services.slurmctld = mkIf (cfg.server.enable) {
path = with pkgs; [ wrappedSlurm munge coreutils ]
++ lib.optional cfg.enableSrunX11 slurm-spank-x11;
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "munged.service" ];
requires = [ "munged.service" ];
serviceConfig = {
Type = "forking";
ExecStart = "${wrappedSlurm}/bin/slurmctld";
PIDFile = "/run/slurmctld.pid";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
preStart = ''
mkdir -p ${cfg.stateSaveLocation}
chown -R ${cfg.user}:slurm ${cfg.stateSaveLocation}
'';
};
systemd.services.slurmdbd = let
# slurm strips the last component off the path
configPath = "$RUNTIME_DIRECTORY/slurmdbd.conf";
in mkIf (cfg.dbdserver.enable) {
path = with pkgs; [ wrappedSlurm munge coreutils ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "munged.service" "mysql.service" ];
requires = [ "munged.service" "mysql.service" ];
preStart = ''
install -m 600 -o ${cfg.user} -T ${slurmdbdConf} ${configPath}
${optionalString (cfg.dbdserver.storagePassFile != null) ''
echo "StoragePass=$(cat ${cfg.dbdserver.storagePassFile})" \
>> ${configPath}
''}
'';
script = ''
export SLURM_CONF=${configPath}
exec ${cfg.package}/bin/slurmdbd -D
'';
serviceConfig = {
RuntimeDirectory = "slurmdbd";
Type = "simple";
PIDFile = "/run/slurmdbd.pid";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View file

@ -0,0 +1,63 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.torque.mom;
torque = pkgs.torque;
momConfig = pkgs.writeText "torque-mom-config" ''
$pbsserver ${cfg.serverNode}
$logevent 225
'';
in
{
options = {
services.torque.mom = {
enable = mkEnableOption "torque computing node";
serverNode = mkOption {
type = types.str;
description = "Hostname running pbs server.";
};
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.torque ];
systemd.services.torque-mom-init = {
path = with pkgs; [ torque util-linux procps inetutils ];
script = ''
pbs_mkdirs -v aux
pbs_mkdirs -v mom
hostname > /var/spool/torque/server_name
cp -v ${momConfig} /var/spool/torque/mom_priv/config
'';
serviceConfig.Type = "oneshot";
unitConfig.ConditionPathExists = "!/var/spool/torque";
};
systemd.services.torque-mom = {
path = [ torque ];
wantedBy = [ "multi-user.target" ];
requires = [ "torque-mom-init.service" ];
after = [ "torque-mom-init.service" "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart = "${torque}/bin/pbs_mom";
PIDFile = "/var/spool/torque/mom_priv/mom.lock";
};
};
};
}

View file

@ -0,0 +1,96 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.torque.server;
torque = pkgs.torque;
in
{
options = {
services.torque.server = {
enable = mkEnableOption "torque server";
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.torque ];
systemd.services.torque-server-init = {
path = with pkgs; [ torque util-linux procps inetutils ];
script = ''
tmpsetup=$(mktemp -t torque-XXXX)
cp -p ${torque}/bin/torque.setup $tmpsetup
sed -i $tmpsetup -e 's/pbs_server -t create/pbs_server -f -t create/'
pbs_mkdirs -v aux
pbs_mkdirs -v server
hostname > /var/spool/torque/server_name
cp -prv ${torque}/var/spool/torque/* /var/spool/torque/
$tmpsetup root
sleep 1
rm -f $tmpsetup
kill $(pgrep pbs_server) 2>/dev/null
kill $(pgrep trqauthd) 2>/dev/null
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig = {
ConditionPathExists = "!/var/spool/torque";
};
};
systemd.services.trqauthd = {
path = [ torque ];
requires = [ "torque-server-init.service" ];
after = [ "torque-server-init.service" ];
serviceConfig = {
Type = "forking";
ExecStart = "${torque}/bin/trqauthd";
};
};
systemd.services.torque-server = {
path = [ torque ];
wantedBy = [ "multi-user.target" ];
wants = [ "torque-scheduler.service" "trqauthd.service" ];
before = [ "trqauthd.service" ];
requires = [ "torque-server-init.service" ];
after = [ "torque-server-init.service" "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart = "${torque}/bin/pbs_server";
ExecStop = "${torque}/bin/qterm";
PIDFile = "/var/spool/torque/server_priv/server.lock";
};
};
systemd.services.torque-scheduler = {
path = [ torque ];
requires = [ "torque-server-init.service" ];
after = [ "torque-server-init.service" ];
serviceConfig = {
Type = "forking";
ExecStart = "${torque}/bin/pbs_sched";
PIDFile = "/var/spool/torque/sched_priv/sched.lock";
};
};
};
}

View file

@ -0,0 +1,290 @@
# NixOS module for Buildbot continous integration server.
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.buildbot-master;
opt = options.services.buildbot-master;
python = cfg.package.pythonModule;
escapeStr = s: escape ["'"] s;
defaultMasterCfg = pkgs.writeText "master.cfg" ''
from buildbot.plugins import *
factory = util.BuildFactory()
c = BuildmasterConfig = dict(
workers = [${concatStringsSep "," cfg.workers}],
protocols = { 'pb': {'port': ${toString cfg.pbPort} } },
title = '${escapeStr cfg.title}',
titleURL = '${escapeStr cfg.titleUrl}',
buildbotURL = '${escapeStr cfg.buildbotUrl}',
db = dict(db_url='${escapeStr cfg.dbUrl}'),
www = dict(port=${toString cfg.port}),
change_source = [ ${concatStringsSep "," cfg.changeSource} ],
schedulers = [ ${concatStringsSep "," cfg.schedulers} ],
builders = [ ${concatStringsSep "," cfg.builders} ],
services = [ ${concatStringsSep "," cfg.reporters} ],
)
for step in [ ${concatStringsSep "," cfg.factorySteps} ]:
factory.addStep(step)
${cfg.extraConfig}
'';
tacFile = pkgs.writeText "buildbot-master.tac" ''
import os
from twisted.application import service
from buildbot.master import BuildMaster
basedir = '${cfg.buildbotDir}'
configfile = '${cfg.masterCfg}'
# Default umask for server
umask = None
# note: this line is matched against to check that this is a buildmaster
# directory; do not edit it.
application = service.Application('buildmaster')
m = BuildMaster(basedir, configfile, umask)
m.setServiceParent(application)
'';
in {
options = {
services.buildbot-master = {
factorySteps = mkOption {
type = types.listOf types.str;
description = "Factory Steps";
default = [];
example = [
"steps.Git(repourl='https://github.com/buildbot/pyflakes.git', mode='incremental')"
"steps.ShellCommand(command=['trial', 'pyflakes'])"
];
};
changeSource = mkOption {
type = types.listOf types.str;
description = "List of Change Sources.";
default = [];
example = [
"changes.GitPoller('https://github.com/buildbot/pyflakes.git', workdir='gitpoller-workdir', branch='master', pollinterval=300)"
];
};
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the Buildbot continuous integration server.";
};
extraConfig = mkOption {
type = types.str;
description = "Extra configuration to append to master.cfg";
default = "c['buildbotNetUsageData'] = None";
};
masterCfg = mkOption {
type = types.path;
description = "Optionally pass master.cfg path. Other options in this configuration will be ignored.";
default = defaultMasterCfg;
defaultText = literalDocBook ''generated configuration file'';
example = "/etc/nixos/buildbot/master.cfg";
};
schedulers = mkOption {
type = types.listOf types.str;
description = "List of Schedulers.";
default = [
"schedulers.SingleBranchScheduler(name='all', change_filter=util.ChangeFilter(branch='master'), treeStableTimer=None, builderNames=['runtests'])"
"schedulers.ForceScheduler(name='force',builderNames=['runtests'])"
];
};
builders = mkOption {
type = types.listOf types.str;
description = "List of Builders.";
default = [
"util.BuilderConfig(name='runtests',workernames=['example-worker'],factory=factory)"
];
};
workers = mkOption {
type = types.listOf types.str;
description = "List of Workers.";
default = [ "worker.Worker('example-worker', 'pass')" ];
};
reporters = mkOption {
default = [];
type = types.listOf types.str;
description = "List of reporter objects used to present build status to various users.";
};
user = mkOption {
default = "buildbot";
type = types.str;
description = "User the buildbot server should execute under.";
};
group = mkOption {
default = "buildbot";
type = types.str;
description = "Primary group of buildbot user.";
};
extraGroups = mkOption {
type = types.listOf types.str;
default = [];
description = "List of extra groups that the buildbot user should be a part of.";
};
home = mkOption {
default = "/home/buildbot";
type = types.path;
description = "Buildbot home directory.";
};
buildbotDir = mkOption {
default = "${cfg.home}/master";
defaultText = literalExpression ''"''${config.${opt.home}}/master"'';
type = types.path;
description = "Specifies the Buildbot directory.";
};
pbPort = mkOption {
default = 9989;
type = types.either types.str types.int;
example = "'tcp:9990:interface=127.0.0.1'";
description = ''
The buildmaster will listen on a TCP port of your choosing
for connections from workers.
It can also use this port for connections from remote Change Sources,
status clients, and debug tools.
This port should be visible to the outside world, and youll need to tell
your worker admins about your choice.
If put in (single) quotes, this can also be used as a connection string,
as defined in the <link xlink:href="https://twistedmatrix.com/documents/current/core/howto/endpoints.html">ConnectionStrings guide</link>.
'';
};
listenAddress = mkOption {
default = "0.0.0.0";
type = types.str;
description = "Specifies the bind address on which the buildbot HTTP interface listens.";
};
buildbotUrl = mkOption {
default = "http://localhost:8010/";
type = types.str;
description = "Specifies the Buildbot URL.";
};
title = mkOption {
default = "Buildbot";
type = types.str;
description = "Specifies the Buildbot Title.";
};
titleUrl = mkOption {
default = "Buildbot";
type = types.str;
description = "Specifies the Buildbot TitleURL.";
};
dbUrl = mkOption {
default = "sqlite:///state.sqlite";
type = types.str;
description = "Specifies the database connection string.";
};
port = mkOption {
default = 8010;
type = types.int;
description = "Specifies port number on which the buildbot HTTP interface listens.";
};
package = mkOption {
type = types.package;
default = pkgs.python3Packages.buildbot-full;
defaultText = literalExpression "pkgs.python3Packages.buildbot-full";
description = "Package to use for buildbot.";
example = literalExpression "pkgs.python3Packages.buildbot";
};
packages = mkOption {
default = [ pkgs.git ];
defaultText = literalExpression "[ pkgs.git ]";
type = types.listOf types.package;
description = "Packages to add to PATH for the buildbot process.";
};
pythonPackages = mkOption {
type = types.functionTo (types.listOf types.package);
default = pythonPackages: with pythonPackages; [ ];
defaultText = literalExpression "pythonPackages: with pythonPackages; [ ]";
description = "Packages to add the to the PYTHONPATH of the buildbot process.";
example = literalExpression "pythonPackages: with pythonPackages; [ requests ]";
};
};
};
config = mkIf cfg.enable {
users.groups = optionalAttrs (cfg.group == "buildbot") {
buildbot = { };
};
users.users = optionalAttrs (cfg.user == "buildbot") {
buildbot = {
description = "Buildbot User.";
isNormalUser = true;
createHome = true;
home = cfg.home;
group = cfg.group;
extraGroups = cfg.extraGroups;
useDefaultShell = true;
};
};
systemd.services.buildbot-master = {
description = "Buildbot Continuous Integration Server.";
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = cfg.packages ++ cfg.pythonPackages python.pkgs;
environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ cfg.package ])}/${python.sitePackages}";
preStart = ''
mkdir -vp "${cfg.buildbotDir}"
# Link the tac file so buildbot command line tools recognize the directory
ln -sf "${tacFile}" "${cfg.buildbotDir}/buildbot.tac"
${cfg.package}/bin/buildbot create-master --db "${cfg.dbUrl}" "${cfg.buildbotDir}"
rm -f buildbot.tac.new master.cfg.sample
'';
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.home;
# NOTE: call twistd directly with stdout logging for systemd
ExecStart = "${python.pkgs.twisted}/bin/twistd -o --nodaemon --pidfile= --logfile - --python ${tacFile}";
};
};
};
imports = [
(mkRenamedOptionModule [ "services" "buildbot-master" "bpPort" ] [ "services" "buildbot-master" "pbPort" ])
(mkRemovedOptionModule [ "services" "buildbot-master" "status" ] ''
Since Buildbot 0.9.0, status targets are deprecated and ignored.
Review your configuration and migrate to reporters (available at services.buildbot-master.reporters).
'')
];
meta.maintainers = with lib.maintainers; [ mic92 lopsided98 ];
}

View file

@ -0,0 +1,198 @@
# NixOS module for Buildbot Worker.
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.buildbot-worker;
opt = options.services.buildbot-worker;
python = cfg.package.pythonModule;
tacFile = pkgs.writeText "aur-buildbot-worker.tac" ''
import os
from io import open
from buildbot_worker.bot import Worker
from twisted.application import service
basedir = '${cfg.buildbotDir}'
# note: this line is matched against to check that this is a worker
# directory; do not edit it.
application = service.Application('buildbot-worker')
master_url_split = '${cfg.masterUrl}'.split(':')
buildmaster_host = master_url_split[0]
port = int(master_url_split[1])
workername = '${cfg.workerUser}'
with open('${cfg.workerPassFile}', 'r', encoding='utf-8') as passwd_file:
passwd = passwd_file.read().strip('\r\n')
keepalive = ${toString cfg.keepalive}
umask = None
maxdelay = 300
numcpus = None
allow_shutdown = None
s = Worker(buildmaster_host, port, workername, passwd, basedir,
keepalive, umask=umask, maxdelay=maxdelay,
numcpus=numcpus, allow_shutdown=allow_shutdown)
s.setServiceParent(application)
'';
in {
options = {
services.buildbot-worker = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the Buildbot Worker.";
};
user = mkOption {
default = "bbworker";
type = types.str;
description = "User the buildbot Worker should execute under.";
};
group = mkOption {
default = "bbworker";
type = types.str;
description = "Primary group of buildbot Worker user.";
};
extraGroups = mkOption {
type = types.listOf types.str;
default = [];
description = "List of extra groups that the Buildbot Worker user should be a part of.";
};
home = mkOption {
default = "/home/bbworker";
type = types.path;
description = "Buildbot home directory.";
};
buildbotDir = mkOption {
default = "${cfg.home}/worker";
defaultText = literalExpression ''"''${config.${opt.home}}/worker"'';
type = types.path;
description = "Specifies the Buildbot directory.";
};
workerUser = mkOption {
default = "example-worker";
type = types.str;
description = "Specifies the Buildbot Worker user.";
};
workerPass = mkOption {
default = "pass";
type = types.str;
description = "Specifies the Buildbot Worker password.";
};
workerPassFile = mkOption {
type = types.path;
description = "File used to store the Buildbot Worker password";
};
hostMessage = mkOption {
default = null;
type = types.nullOr types.str;
description = "Description of this worker";
};
adminMessage = mkOption {
default = null;
type = types.nullOr types.str;
description = "Name of the administrator of this worker";
};
masterUrl = mkOption {
default = "localhost:9989";
type = types.str;
description = "Specifies the Buildbot Worker connection string.";
};
keepalive = mkOption {
default = 600;
type = types.int;
description = "
This is a number that indicates how frequently keepalive messages should be sent
from the worker to the buildmaster, expressed in seconds.
";
};
package = mkOption {
type = types.package;
default = pkgs.python3Packages.buildbot-worker;
defaultText = literalExpression "pkgs.python3Packages.buildbot-worker";
description = "Package to use for buildbot worker.";
example = literalExpression "pkgs.python2Packages.buildbot-worker";
};
packages = mkOption {
default = with pkgs; [ git ];
defaultText = literalExpression "[ pkgs.git ]";
type = types.listOf types.package;
description = "Packages to add to PATH for the buildbot process.";
};
};
};
config = mkIf cfg.enable {
services.buildbot-worker.workerPassFile = mkDefault (pkgs.writeText "buildbot-worker-password" cfg.workerPass);
users.groups = optionalAttrs (cfg.group == "bbworker") {
bbworker = { };
};
users.users = optionalAttrs (cfg.user == "bbworker") {
bbworker = {
description = "Buildbot Worker User.";
isNormalUser = true;
createHome = true;
home = cfg.home;
group = cfg.group;
extraGroups = cfg.extraGroups;
useDefaultShell = true;
};
};
systemd.services.buildbot-worker = {
description = "Buildbot Worker.";
after = [ "network.target" "buildbot-master.service" ];
wantedBy = [ "multi-user.target" ];
path = cfg.packages;
environment.PYTHONPATH = "${python.withPackages (p: [ cfg.package ])}/${python.sitePackages}";
preStart = ''
mkdir -vp "${cfg.buildbotDir}/info"
${optionalString (cfg.hostMessage != null) ''
ln -sf "${pkgs.writeText "buildbot-worker-host" cfg.hostMessage}" "${cfg.buildbotDir}/info/host"
''}
${optionalString (cfg.adminMessage != null) ''
ln -sf "${pkgs.writeText "buildbot-worker-admin" cfg.adminMessage}" "${cfg.buildbotDir}/info/admin"
''}
'';
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.home;
# NOTE: call twistd directly with stdout logging for systemd
ExecStart = "${python.pkgs.twisted}/bin/twistd --nodaemon --pidfile= --logfile - --python ${tacFile}";
};
};
};
meta.maintainers = with lib.maintainers; [ ];
}

View file

@ -0,0 +1,280 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.buildkite-agents;
mkHookOption = { name, description, example ? null }: {
inherit name;
value = mkOption {
default = null;
inherit description;
type = types.nullOr types.lines;
} // (if example == null then {} else { inherit example; });
};
mkHookOptions = hooks: listToAttrs (map mkHookOption hooks);
hooksDir = cfg: let
mkHookEntry = name: value: ''
cat > $out/${name} <<'EOF'
#! ${pkgs.runtimeShell}
set -e
${value}
EOF
chmod 755 $out/${name}
'';
in pkgs.runCommand "buildkite-agent-hooks" { preferLocalBuild = true; } ''
mkdir $out
${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))}
'';
buildkiteOptions = { name ? "", config, ... }: {
options = {
enable = mkOption {
default = true;
type = types.bool;
description = "Whether to enable this buildkite agent";
};
package = mkOption {
default = pkgs.buildkite-agent;
defaultText = literalExpression "pkgs.buildkite-agent";
description = "Which buildkite-agent derivation to use";
type = types.package;
};
dataDir = mkOption {
default = "/var/lib/buildkite-agent-${name}";
description = "The workdir for the agent";
type = types.str;
};
runtimePackages = mkOption {
default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
defaultText = literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
description = "Add programs to the buildkite-agent environment";
type = types.listOf types.package;
};
tokenPath = mkOption {
type = types.path;
description = ''
The token from your Buildkite "Agents" page.
A run-time path to the token file, which is supposed to be provisioned
outside of Nix store.
'';
};
name = mkOption {
type = types.str;
default = "%hostname-${name}-%n";
description = ''
The name of the agent as seen in the buildkite dashboard.
'';
};
tags = mkOption {
type = types.attrsOf (types.either types.str (types.listOf types.str));
default = {};
example = { queue = "default"; docker = "true"; ruby2 ="true"; };
description = ''
Tags for the agent.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = "debug=true";
description = ''
Extra lines to be added verbatim to the configuration file.
'';
};
privateSshKeyPath = mkOption {
type = types.nullOr types.path;
default = null;
## maximum care is taken so that secrets (ssh keys and the CI token)
## don't end up in the Nix store.
apply = final: if final == null then null else toString final;
description = ''
OpenSSH private key
A run-time path to the key file, which is supposed to be provisioned
outside of Nix store.
'';
};
hooks = mkHookOptions [
{ name = "checkout";
description = ''
The `checkout` hook script will replace the default checkout routine of the
bootstrap.sh script. You can use this hook to do your own SCM checkout
behaviour
''; }
{ name = "command";
description = ''
The `command` hook script will replace the default implementation of running
the build command.
''; }
{ name = "environment";
description = ''
The `environment` hook will run before all other commands, and can be used
to set up secrets, data, etc. Anything exported in hooks will be available
to the build script.
Note: the contents of this file will be copied to the world-readable
Nix store.
'';
example = ''
export SECRET_VAR=`head -1 /run/keys/secret`
''; }
{ name = "post-artifact";
description = ''
The `post-artifact` hook will run just after artifacts are uploaded
''; }
{ name = "post-checkout";
description = ''
The `post-checkout` hook will run after the bootstrap script has checked out
your projects source code.
''; }
{ name = "post-command";
description = ''
The `post-command` hook will run after the bootstrap script has run your
build commands
''; }
{ name = "pre-artifact";
description = ''
The `pre-artifact` hook will run just before artifacts are uploaded
''; }
{ name = "pre-checkout";
description = ''
The `pre-checkout` hook will run just before your projects source code is
checked out from your SCM provider
''; }
{ name = "pre-command";
description = ''
The `pre-command` hook will run just before your build command runs
''; }
{ name = "pre-exit";
description = ''
The `pre-exit` hook will run just before your build job finishes
''; }
];
hooksPath = mkOption {
type = types.path;
default = hooksDir config;
defaultText = literalDocBook "generated from <option>services.buildkite-agents.&lt;name&gt;.hooks</option>";
description = ''
Path to the directory storing the hooks.
Consider using <option>services.buildkite-agents.&lt;name&gt;.hooks.&lt;name&gt;</option>
instead.
'';
};
shell = mkOption {
type = types.str;
default = "${pkgs.bash}/bin/bash -e -c";
defaultText = literalExpression ''"''${pkgs.bash}/bin/bash -e -c"'';
description = ''
Command that buildkite-agent 3 will execute when it spawns a shell.
'';
};
};
};
enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
in
{
options.services.buildkite-agents = mkOption {
type = types.attrsOf (types.submodule buildkiteOptions);
default = {};
description = ''
Attribute set of buildkite agents.
The attribute key is combined with the hostname and a unique integer to
create the final agent name. This can be overridden by setting the `name`
attribute.
'';
};
config.users.users = mapAgents (name: cfg: {
"buildkite-agent-${name}" = {
name = "buildkite-agent-${name}";
home = cfg.dataDir;
createHome = true;
description = "Buildkite agent user";
extraGroups = [ "keys" ];
isSystemUser = true;
group = "buildkite-agent-${name}";
};
});
config.users.groups = mapAgents (name: cfg: {
"buildkite-agent-${name}" = {};
});
config.systemd.services = mapAgents (name: cfg: {
"buildkite-agent-${name}" =
{ description = "Buildkite Agent";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ];
environment = config.networking.proxy.envVars // {
HOME = cfg.dataDir;
NIX_REMOTE = "daemon";
};
## NB: maximum care is taken so that secrets (ssh keys and the CI token)
## don't end up in the Nix store.
preStart = let
sshDir = "${cfg.dataDir}/.ssh";
tagStr = name: value:
if lib.isList value
then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value)
else "${name}=${value}";
tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags);
in
optionalString (cfg.privateSshKeyPath != null) ''
mkdir -m 0700 -p "${sshDir}"
install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
'' + ''
cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
token="$(cat ${toString cfg.tokenPath})"
name="${cfg.name}"
shell="${cfg.shell}"
tags="${tagsStr}"
build-path="${cfg.dataDir}/builds"
hooks-path="${cfg.hooksPath}"
${cfg.extraConfig}
EOF
'';
serviceConfig =
{ ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg";
User = "buildkite-agent-${name}";
RestartSec = 5;
Restart = "on-failure";
TimeoutSec = 10;
# set a long timeout to give buildkite-agent a chance to finish current builds
TimeoutStopSec = "2 min";
KillMode = "mixed";
};
};
});
config.assertions = mapAgents (name: cfg: [
{ assertion = cfg.hooksPath == (hooksDir cfg) || all (v: v == null) (attrValues cfg.hooks);
message = ''
Options `services.buildkite-agents.${name}.hooksPath' and
`services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.
'';
}
]);
imports = [
(mkRemovedOptionModule [ "services" "buildkite-agent"] "services.buildkite-agent has been upgraded from version 2 to version 3 and moved to an attribute set at services.buildkite-agents. Please consult the 20.03 release notes for more information.")
];
}

View file

@ -0,0 +1,320 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.github-runner;
svcName = "github-runner";
systemdDir = "${svcName}/${cfg.name}";
# %t: Runtime directory root (usually /run); see systemd.unit(5)
runtimeDir = "%t/${systemdDir}";
# %S: State directory root (usually /var/lib); see systemd.unit(5)
stateDir = "%S/${systemdDir}";
# %L: Log directory root (usually /var/log); see systemd.unit(5)
logsDir = "%L/${systemdDir}";
# Name of file stored in service state directory
currentConfigTokenFilename = ".current-token";
in
{
options.services.github-runner = {
enable = mkOption {
default = false;
example = true;
description = ''
Whether to enable GitHub Actions runner.
Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
<link xlink:href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners"
>About self-hosted runners</link>.
'';
type = lib.types.bool;
};
url = mkOption {
type = types.str;
description = ''
Repository to add the runner to.
Changing this option triggers a new runner registration.
IMPORTANT: If your token is org-wide (not per repository), you need to
provide a github org link, not a single repository, so do it like this
<literal>https://github.com/nixos</literal>, not like this
<literal>https://github.com/nixos/nixpkgs</literal>.
Otherwise, you are going to get a <literal>404 NotFound</literal>
from <literal>POST https://api.github.com/actions/runner-registration</literal>
in the configure script.
'';
example = "https://github.com/nixos/nixpkgs";
};
tokenFile = mkOption {
type = types.path;
description = ''
The full path to a file which contains the runner registration token.
The file should contain exactly one line with the token without any newline.
The token can be used to re-register a runner of the same name but is time-limited.
Changing this option or the file's content triggers a new runner registration.
'';
example = "/run/secrets/github-runner/nixos.token";
};
name = mkOption {
# Same pattern as for `networking.hostName`
type = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
description = ''
Name of the runner to configure. Defaults to the hostname.
Changing this option triggers a new runner registration.
'';
example = "nixos";
default = config.networking.hostName;
defaultText = literalExpression "config.networking.hostName";
};
runnerGroup = mkOption {
type = types.nullOr types.str;
description = ''
Name of the runner group to add this runner to (defaults to the default runner group).
Changing this option triggers a new runner registration.
'';
default = null;
};
extraLabels = mkOption {
type = types.listOf types.str;
description = ''
Extra labels in addition to the default (<literal>["self-hosted", "Linux", "X64"]</literal>).
Changing this option triggers a new runner registration.
'';
example = literalExpression ''[ "nixos" ]'';
default = [ ];
};
replace = mkOption {
type = types.bool;
description = ''
Replace any existing runner with the same name.
Without this flag, registering a new runner with the same name fails.
'';
default = false;
};
extraPackages = mkOption {
type = types.listOf types.package;
description = ''
Extra packages to add to <literal>PATH</literal> of the service to make them available to workflows.
'';
default = [ ];
};
package = mkOption {
type = types.package;
description = ''
Which github-runner derivation to use.
'';
default = pkgs.github-runner;
defaultText = literalExpression "pkgs.github-runner";
};
};
config = mkIf cfg.enable {
warnings = optionals (isStorePath cfg.tokenFile) [
''
`services.github-runner.tokenFile` points to the Nix store and, therefore, is world-readable.
Consider using a path outside of the Nix store to keep the token private.
''
];
systemd.services.${svcName} = {
description = "GitHub Actions runner";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network.target" "network-online.target" ];
environment = {
HOME = runtimeDir;
RUNNER_ROOT = runtimeDir;
};
path = (with pkgs; [
bash
coreutils
git
gnutar
gzip
]) ++ [
config.nix.package
] ++ cfg.extraPackages;
serviceConfig = rec {
ExecStart = "${cfg.package}/bin/runsvc.sh";
# Does the following, sequentially:
# - If the module configuration or the token has changed, purge the state directory,
# and create the current and the new token file with the contents of the configured
# token. While both files have the same content, only the later is accessible by
# the service user.
# - Configure the runner using the new token file. When finished, delete it.
# - Set up the directory structure by creating the necessary symlinks.
ExecStartPre =
let
# Wrapper script which expects the full path of the state, runtime and logs
# directory as arguments. Overrides the respective systemd variables to provide
# unambiguous directory names. This becomes relevant, for example, if the
# caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
# to contain more than one directory. This causes systemd to set the respective
# environment variables with the path of all of the given directories, separated
# by a colon.
writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
set -euo pipefail
STATE_DIRECTORY="$1"
RUNTIME_DIRECTORY="$2"
LOGS_DIRECTORY="$3"
${lines}
'';
currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" ] cfg;
newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
newConfigTokenFilename = ".new-token";
runnerCredFiles = [
".credentials"
".credentials_rsaparams"
".runner"
];
unconfigureRunner = writeScript "unconfigure" ''
differs=
# Set `differs = 1` if current and new runner config differ or if `currentConfigPath` does not exist
${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 || differs=1
# Also trigger a registration if the token content changed
${pkgs.diffutils}/bin/diff -q \
"$STATE_DIRECTORY"/${currentConfigTokenFilename} \
${escapeShellArg cfg.tokenFile} \
>/dev/null 2>&1 || differs=1
if [[ -n "$differs" ]]; then
echo "Config has changed, removing old runner state."
echo "The old runner will still appear in the GitHub Actions UI." \
"You have to remove it manually."
find "$STATE_DIRECTORY/" -mindepth 1 -delete
# Copy the configured token file to the state dir and allow the service user to read the file
install --mode=666 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${newConfigTokenFilename}"
# Also copy current file to allow for a diff on the next start
install --mode=600 ${escapeShellArg cfg.tokenFile} "$STATE_DIRECTORY/${currentConfigTokenFilename}"
fi
'';
configureRunner = writeScript "configure" ''
if [[ -e "$STATE_DIRECTORY/${newConfigTokenFilename}" ]]; then
echo "Configuring GitHub Actions Runner"
token=$(< "$STATE_DIRECTORY"/${newConfigTokenFilename})
RUNNER_ROOT="$STATE_DIRECTORY" ${cfg.package}/bin/config.sh \
--unattended \
--disableupdate \
--work "$RUNTIME_DIRECTORY" \
--url ${escapeShellArg cfg.url} \
--token "$token" \
--labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} \
--name ${escapeShellArg cfg.name} \
${optionalString cfg.replace "--replace"} \
${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
# Move the automatically created _diag dir to the logs dir
mkdir -p "$STATE_DIRECTORY/_diag"
cp -r "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
rm -rf "$STATE_DIRECTORY/_diag/"
# Cleanup token from config
rm "$STATE_DIRECTORY/${newConfigTokenFilename}"
# Symlink to new config
ln -s '${newConfigPath}' "${currentConfigPath}"
fi
'';
setupRuntimeDir = writeScript "setup-runtime-dirs" ''
# Link _diag dir
ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag"
# Link the runner credentials to the runtime dir
ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/"
'';
in
map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
"+${unconfigureRunner}" # runs as root
configureRunner
setupRuntimeDir
];
# Contains _diag
LogsDirectory = [ systemdDir ];
# Default RUNNER_ROOT which contains ephemeral Runner data
RuntimeDirectory = [ systemdDir ];
# Home of persistent runner data, e.g., credentials
StateDirectory = [ systemdDir ];
StateDirectoryMode = "0700";
WorkingDirectory = runtimeDir;
InaccessiblePaths = [
# Token file path given in the configuration
cfg.tokenFile
# Token file in the state directory
"${stateDir}/${currentConfigTokenFilename}"
];
# By default, use a dynamically allocated user
DynamicUser = true;
KillMode = "process";
KillSignal = "SIGTERM";
# Hardening (may overlap with DynamicUser=)
# The following options are only for optimizing:
# systemd-analyze security github-runner
AmbientCapabilities = "";
CapabilityBoundingSet = "";
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
UMask = "0066";
ProtectProc = "invisible";
ProcSubset = "pid";
SystemCallFilter = [
"~@debug"
"~@mount"
"~@privileged"
"~@cpu-emulation"
"~@obsolete"
];
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
# Needs network access
PrivateNetwork = false;
# Cannot be true due to Node
MemoryDenyWriteExecute = false;
};
};
};
}

View file

@ -0,0 +1,588 @@
{ config, lib, pkgs, ... }:
with builtins;
with lib;
let
cfg = config.services.gitlab-runner;
hasDocker = config.virtualisation.docker.enable;
hashedServices = mapAttrs'
(name: service: nameValuePair
"${name}_${config.networking.hostName}_${
substring 0 12
(hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
service)
cfg.services;
configPath = "$HOME/.gitlab-runner/config.toml";
configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
if (cfg.configFile != null) then ''
mkdir -p $(dirname ${configPath})
cp ${cfg.configFile} ${configPath}
# make config file readable by service
chown -R --reference=$HOME $(dirname ${configPath})
'' else ''
export CONFIG_FILE=${configPath}
mkdir -p $(dirname ${configPath})
# remove no longer existing services
gitlab-runner verify --delete
# current and desired state
NEEDED_SERVICES=$(echo ${concatStringsSep " " (attrNames hashedServices)} | tr " " "\n")
REGISTERED_SERVICES=$(gitlab-runner list 2>&1 | grep 'Executor' | awk '{ print $1 }')
# difference between current and desired state
NEW_SERVICES=$(grep -vxF -f <(echo "$REGISTERED_SERVICES") <(echo "$NEEDED_SERVICES") || true)
OLD_SERVICES=$(grep -vxF -f <(echo "$NEEDED_SERVICES") <(echo "$REGISTERED_SERVICES") || true)
# register new services
${concatStringsSep "\n" (mapAttrsToList (name: service: ''
if echo "$NEW_SERVICES" | grep -xq "${name}"; then
bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
"set -a && source ${service.registrationConfigFile} &&"
"gitlab-runner register"
"--non-interactive"
(if service.description != null then "--description \"${service.description}\"" else "--name '${name}'")
"--executor ${service.executor}"
"--limit ${toString service.limit}"
"--request-concurrency ${toString service.requestConcurrency}"
"--maximum-timeout ${toString service.maximumTimeout}"
] ++ service.registrationFlags
++ optional (service.buildsDir != null)
"--builds-dir ${service.buildsDir}"
++ optional (service.cloneUrl != null)
"--clone-url ${service.cloneUrl}"
++ optional (service.preCloneScript != null)
"--pre-clone-script ${service.preCloneScript}"
++ optional (service.preBuildScript != null)
"--pre-build-script ${service.preBuildScript}"
++ optional (service.postBuildScript != null)
"--post-build-script ${service.postBuildScript}"
++ optional (service.tagList != [ ])
"--tag-list ${concatStringsSep "," service.tagList}"
++ optional service.runUntagged
"--run-untagged"
++ optional service.protected
"--access-level ref_protected"
++ optional service.debugTraceDisabled
"--debug-trace-disabled"
++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
++ optionals (hasPrefix "docker" service.executor) (
assert (
assertMsg (service.dockerImage != null)
"dockerImage option is required for ${service.executor} executor (${name})");
[ "--docker-image ${service.dockerImage}" ]
++ optional service.dockerDisableCache
"--docker-disable-cache"
++ optional service.dockerPrivileged
"--docker-privileged"
++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
)
))} && sleep 1 || exit 1
fi
'') hashedServices)}
# unregister old services
for NAME in $(echo "$OLD_SERVICES")
do
[ ! -z "$NAME" ] && gitlab-runner unregister \
--name "$NAME" && sleep 1
done
# update global options
remarshal --if toml --of json ${configPath} \
| jq -cM ${escapeShellArg (concatStringsSep " | " [
".check_interval = ${toJSON cfg.checkInterval}"
".concurrent = ${toJSON cfg.concurrent}"
".sentry_dsn = ${toJSON cfg.sentryDSN}"
".listen_address = ${toJSON cfg.prometheusListenAddress}"
".session_server.listen_address = ${toJSON cfg.sessionServer.listenAddress}"
".session_server.advertise_address = ${toJSON cfg.sessionServer.advertiseAddress}"
".session_server.session_timeout = ${toJSON cfg.sessionServer.sessionTimeout}"
"del(.[] | nulls)"
"del(.session_server[] | nulls)"
])} \
| remarshal --if json --of toml \
| sponge ${configPath}
# make config file readable by service
chown -R --reference=$HOME $(dirname ${configPath})
'');
startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
export CONFIG_FILE=${configPath}
exec gitlab-runner run --working-directory $HOME
'';
in
{
options.services.gitlab-runner = {
enable = mkEnableOption "Gitlab Runner";
configFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Configuration file for gitlab-runner.
<option>configFile</option> takes precedence over <option>services</option>.
<option>checkInterval</option> and <option>concurrent</option> will be ignored too.
This option is deprecated, please use <option>services</option> instead.
You can use <option>registrationConfigFile</option> and
<option>registrationFlags</option>
for settings not covered by this module.
'';
};
checkInterval = mkOption {
type = types.int;
default = 0;
example = literalExpression "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3";
description = ''
Defines the interval length, in seconds, between new jobs check.
The default value is 3;
if set to 0 or lower, the default value will be used.
See <link xlink:href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#how-check_interval-works">runner documentation</link> for more information.
'';
};
concurrent = mkOption {
type = types.int;
default = 1;
example = literalExpression "config.nix.settings.max-jobs";
description = ''
Limits how many jobs globally can be run concurrently.
The most upper limit of jobs using all defined runners.
0 does not mean unlimited.
'';
};
sentryDSN = mkOption {
type = types.nullOr types.str;
default = null;
example = "https://public:private@host:port/1";
description = ''
Data Source Name for tracking of all system level errors to Sentry.
'';
};
prometheusListenAddress = mkOption {
type = types.nullOr types.str;
default = null;
example = "localhost:8080";
description = ''
Address (&lt;host&gt;:&lt;port&gt;) on which the Prometheus metrics HTTP server
should be listening.
'';
};
sessionServer = mkOption {
type = types.submodule {
options = {
listenAddress = mkOption {
type = types.nullOr types.str;
default = null;
example = "0.0.0.0:8093";
description = ''
An internal URL to be used for the session server.
'';
};
advertiseAddress = mkOption {
type = types.nullOr types.str;
default = null;
example = "runner-host-name.tld:8093";
description = ''
The URL that the Runner will expose to GitLab to be used
to access the session server.
Fallbacks to <option>listenAddress</option> if not defined.
'';
};
sessionTimeout = mkOption {
type = types.int;
default = 1800;
description = ''
How long in seconds the session can stay active after
the job completes (which will block the job from finishing).
'';
};
};
};
default = { };
example = literalExpression ''
{
listenAddress = "0.0.0.0:8093";
}
'';
description = ''
The session server allows the user to interact with jobs
that the Runner is responsible for. A good example of this is the
<link xlink:href="https://docs.gitlab.com/ee/ci/interactive_web_terminal/index.html">interactive web terminal</link>.
'';
};
gracefulTermination = mkOption {
type = types.bool;
default = false;
description = ''
Finish all remaining jobs before stopping.
If not set gitlab-runner will stop immediatly without waiting
for jobs to finish, which will lead to failed builds.
'';
};
gracefulTimeout = mkOption {
type = types.str;
default = "infinity";
example = "5min 20s";
description = ''
Time to wait until a graceful shutdown is turned into a forceful one.
'';
};
package = mkOption {
type = types.package;
default = pkgs.gitlab-runner;
defaultText = literalExpression "pkgs.gitlab-runner";
example = literalExpression "pkgs.gitlab-runner_1_11";
description = "Gitlab Runner package to use.";
};
extraPackages = mkOption {
type = types.listOf types.package;
default = [ ];
description = ''
Extra packages to add to PATH for the gitlab-runner process.
'';
};
services = mkOption {
description = "GitLab Runner services.";
default = { };
example = literalExpression ''
{
# runner for building in docker via host's nix-daemon
# nix store will be readable in runner, might be insecure
nix = {
# File should contain at least these two variables:
# `CI_SERVER_URL`
# `REGISTRATION_TOKEN`
registrationConfigFile = "/run/secrets/gitlab-runner-registration";
dockerImage = "alpine";
dockerVolumes = [
"/nix/store:/nix/store:ro"
"/nix/var/nix/db:/nix/var/nix/db:ro"
"/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
];
dockerDisableCache = true;
preBuildScript = pkgs.writeScript "setup-container" '''
mkdir -p -m 0755 /nix/var/log/nix/drvs
mkdir -p -m 0755 /nix/var/nix/gcroots
mkdir -p -m 0755 /nix/var/nix/profiles
mkdir -p -m 0755 /nix/var/nix/temproots
mkdir -p -m 0755 /nix/var/nix/userpool
mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
mkdir -p -m 1777 /nix/var/nix/profiles/per-user
mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
mkdir -p -m 0700 "$HOME/.nix-defexpr"
. ''${pkgs.nix}/etc/profile.d/nix.sh
''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}
''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
''${pkgs.nix}/bin/nix-channel --update nixpkgs
''';
environmentVariables = {
ENV = "/etc/profile";
USER = "root";
NIX_REMOTE = "daemon";
PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
};
tagList = [ "nix" ];
};
# runner for building docker images
docker-images = {
# File should contain at least these two variables:
# `CI_SERVER_URL`
# `REGISTRATION_TOKEN`
registrationConfigFile = "/run/secrets/gitlab-runner-registration";
dockerImage = "docker:stable";
dockerVolumes = [
"/var/run/docker.sock:/var/run/docker.sock"
];
tagList = [ "docker-images" ];
};
# runner for executing stuff on host system (very insecure!)
# make sure to add required packages (including git!)
# to `environment.systemPackages`
shell = {
# File should contain at least these two variables:
# `CI_SERVER_URL`
# `REGISTRATION_TOKEN`
registrationConfigFile = "/run/secrets/gitlab-runner-registration";
executor = "shell";
tagList = [ "shell" ];
};
# runner for everything else
default = {
# File should contain at least these two variables:
# `CI_SERVER_URL`
# `REGISTRATION_TOKEN`
registrationConfigFile = "/run/secrets/gitlab-runner-registration";
dockerImage = "debian:stable";
};
}
'';
type = types.attrsOf (types.submodule {
options = {
registrationConfigFile = mkOption {
type = types.path;
description = ''
Absolute path to a file with environment variables
used for gitlab-runner registration.
A list of all supported environment variables can be found in
<literal>gitlab-runner register --help</literal>.
Ones that you probably want to set is
<literal>CI_SERVER_URL=&lt;CI server URL&gt;</literal>
<literal>REGISTRATION_TOKEN=&lt;registration secret&gt;</literal>
WARNING: make sure to use quoted absolute path,
or it is going to be copied to Nix Store.
'';
};
registrationFlags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--docker-helper-image my/gitlab-runner-helper" ];
description = ''
Extra command-line flags passed to
<literal>gitlab-runner register</literal>.
Execute <literal>gitlab-runner register --help</literal>
for a list of supported flags.
'';
};
environmentVariables = mkOption {
type = types.attrsOf types.str;
default = { };
example = { NAME = "value"; };
description = ''
Custom environment variables injected to build environment.
For secrets you can use <option>registrationConfigFile</option>
with <literal>RUNNER_ENV</literal> variable set.
'';
};
description = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Name/description of the runner.
'';
};
executor = mkOption {
type = types.str;
default = "docker";
description = ''
Select executor, eg. shell, docker, etc.
See <link xlink:href="https://docs.gitlab.com/runner/executors/README.html">runner documentation</link> for more information.
'';
};
buildsDir = mkOption {
type = types.nullOr types.path;
default = null;
example = "/var/lib/gitlab-runner/builds";
description = ''
Absolute path to a directory where builds will be stored
in context of selected executor (Locally, Docker, SSH).
'';
};
cloneUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = "http://gitlab.example.local";
description = ''
Overwrite the URL for the GitLab instance. Used if the Runner cant connect to GitLab on the URL GitLab exposes itself.
'';
};
dockerImage = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Docker image to be used.
'';
};
dockerVolumes = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
description = ''
Bind-mount a volume and create it
if it doesn't exist prior to mounting.
'';
};
dockerDisableCache = mkOption {
type = types.bool;
default = false;
description = ''
Disable all container caching.
'';
};
dockerPrivileged = mkOption {
type = types.bool;
default = false;
description = ''
Give extended privileges to container.
'';
};
dockerExtraHosts = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "other-host:127.0.0.1" ];
description = ''
Add a custom host-to-IP mapping.
'';
};
dockerAllowedImages = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
description = ''
Whitelist allowed images.
'';
};
dockerAllowedServices = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "postgres:9" "redis:*" "mysql:*" ];
description = ''
Whitelist allowed services.
'';
};
preCloneScript = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Runner-specific command script executed before code is pulled.
'';
};
preBuildScript = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Runner-specific command script executed after code is pulled,
just before build executes.
'';
};
postBuildScript = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Runner-specific command script executed after code is pulled
and just after build executes.
'';
};
tagList = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Tag list.
'';
};
runUntagged = mkOption {
type = types.bool;
default = false;
description = ''
Register to run untagged builds; defaults to
<literal>true</literal> when <option>tagList</option> is empty.
'';
};
limit = mkOption {
type = types.int;
default = 0;
description = ''
Limit how many jobs can be handled concurrently by this service.
0 (default) simply means don't limit.
'';
};
requestConcurrency = mkOption {
type = types.int;
default = 0;
description = ''
Limit number of concurrent requests for new jobs from GitLab.
'';
};
maximumTimeout = mkOption {
type = types.int;
default = 0;
description = ''
What is the maximum timeout (in seconds) that will be set for
job when using this Runner. 0 (default) simply means don't limit.
'';
};
protected = mkOption {
type = types.bool;
default = false;
description = ''
When set to true Runner will only run on pipelines
triggered on protected branches.
'';
};
debugTraceDisabled = mkOption {
type = types.bool;
default = false;
description = ''
When set to true Runner will disable the possibility of
using the <literal>CI_DEBUG_TRACE</literal> feature.
'';
};
};
});
};
};
config = mkIf cfg.enable {
warnings = (mapAttrsToList
(n: v: "services.gitlab-runner.services.${n}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
(filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services))
++ optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
environment.systemPackages = [ cfg.package ];
systemd.services.gitlab-runner = {
description = "Gitlab Runner";
documentation = [ "https://docs.gitlab.com/runner/" ];
after = [ "network.target" ]
++ optional hasDocker "docker.service";
requires = optional hasDocker "docker.service";
wantedBy = [ "multi-user.target" ];
environment = config.networking.proxy.envVars // {
HOME = "/var/lib/gitlab-runner";
};
path = with pkgs; [
bash
gawk
jq
moreutils
remarshal
util-linux
cfg.package
] ++ cfg.extraPackages;
reloadIfChanged = true;
serviceConfig = {
# Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
# to `lib.mkForce false` in your configuration to run this service as root.
# You can also set `User` and `Group` options to run this service as desired user.
# Make sure to restart service or changes won't apply.
DynamicUser = true;
StateDirectory = "gitlab-runner";
SupplementaryGroups = optional hasDocker "docker";
ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
ExecStart = "${startScript}/bin/gitlab-runner-start";
ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
} // optionalAttrs (cfg.gracefulTermination) {
TimeoutStopSec = "${cfg.gracefulTimeout}";
KillSignal = "SIGQUIT";
KillMode = "process";
};
};
# Enable docker if `docker` executor is used in any service
virtualisation.docker.enable = mkIf (
any (s: s.executor == "docker") (attrValues cfg.services)
) (mkDefault true);
};
imports = [
(mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
(mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
(mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
];
}

View file

@ -0,0 +1,218 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.gocd-agent;
opt = options.services.gocd-agent;
in {
options = {
services.gocd-agent = {
enable = mkEnableOption "gocd-agent";
user = mkOption {
default = "gocd-agent";
type = types.str;
description = ''
User the Go.CD agent should execute under.
'';
};
group = mkOption {
default = "gocd-agent";
type = types.str;
description = ''
If the default user "gocd-agent" is configured then this is the primary
group of that user.
'';
};
extraGroups = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "wheel" "docker" ];
description = ''
List of extra groups that the "gocd-agent" user should be a part of.
'';
};
packages = mkOption {
default = [ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ];
defaultText = literalExpression "[ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ]";
type = types.listOf types.package;
description = ''
Packages to add to PATH for the Go.CD agent process.
'';
};
agentConfig = mkOption {
default = "";
type = types.str;
example = ''
agent.auto.register.resources=ant,java
agent.auto.register.environments=QA,Performance
agent.auto.register.hostname=Agent01
'';
description = ''
Agent registration configuration.
'';
};
goServer = mkOption {
default = "https://127.0.0.1:8154/go";
type = types.str;
description = ''
URL of the GoCD Server to attach the Go.CD Agent to.
'';
};
workDir = mkOption {
default = "/var/lib/go-agent";
type = types.str;
description = ''
Specifies the working directory in which the Go.CD agent java archive resides.
'';
};
initialJavaHeapSize = mkOption {
default = "128m";
type = types.str;
description = ''
Specifies the initial java heap memory size for the Go.CD agent java process.
'';
};
maxJavaHeapMemory = mkOption {
default = "256m";
type = types.str;
description = ''
Specifies the java maximum heap memory size for the Go.CD agent java process.
'';
};
startupOptions = mkOption {
type = types.listOf types.str;
default = [
"-Xms${cfg.initialJavaHeapSize}"
"-Xmx${cfg.maxJavaHeapMemory}"
"-Djava.io.tmpdir=/tmp"
"-Dcruise.console.publish.interval=10"
"-Djava.security.egd=file:/dev/./urandom"
];
defaultText = literalExpression ''
[
"-Xms''${config.${opt.initialJavaHeapSize}}"
"-Xmx''${config.${opt.maxJavaHeapMemory}}"
"-Djava.io.tmpdir=/tmp"
"-Dcruise.console.publish.interval=10"
"-Djava.security.egd=file:/dev/./urandom"
]
'';
description = ''
Specifies startup command line arguments to pass to Go.CD agent
java process.
'';
};
extraOptions = mkOption {
default = [ ];
type = types.listOf types.str;
example = [
"-X debug"
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"
"-verbose:gc"
"-Xloggc:go-agent-gc.log"
"-XX:+PrintGCTimeStamps"
"-XX:+PrintTenuringDistribution"
"-XX:+PrintGCDetails"
"-XX:+PrintGC"
];
description = ''
Specifies additional command line arguments to pass to Go.CD agent
java process. Example contains debug and gcLog arguments.
'';
};
environment = mkOption {
default = { };
type = with types; attrsOf str;
description = ''
Additional environment variables to be passed to the Go.CD agent process.
As a base environment, Go.CD agent receives NIX_PATH from
<option>environment.sessionVariables</option>, NIX_REMOTE is set to
"daemon".
'';
};
};
};
config = mkIf cfg.enable {
users.groups = optionalAttrs (cfg.group == "gocd-agent") {
gocd-agent.gid = config.ids.gids.gocd-agent;
};
users.users = optionalAttrs (cfg.user == "gocd-agent") {
gocd-agent = {
description = "gocd-agent user";
createHome = true;
home = cfg.workDir;
group = cfg.group;
extraGroups = cfg.extraGroups;
useDefaultShell = true;
uid = config.ids.uids.gocd-agent;
};
};
systemd.services.gocd-agent = {
description = "GoCD Agent";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment =
let
selectedSessionVars =
lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ])
config.environment.sessionVariables;
in
selectedSessionVars //
{
NIX_REMOTE = "daemon";
AGENT_WORK_DIR = cfg.workDir;
AGENT_STARTUP_ARGS = ''${concatStringsSep " " cfg.startupOptions}'';
LOG_DIR = cfg.workDir;
LOG_FILE = "${cfg.workDir}/go-agent-start.log";
} //
cfg.environment;
path = cfg.packages;
script = ''
MPATH="''${PATH}";
source /etc/profile
export PATH="''${MPATH}:''${PATH}";
if ! test -f ~/.nixpkgs/config.nix; then
mkdir -p ~/.nixpkgs/
echo "{ allowUnfree = true; }" > ~/.nixpkgs/config.nix
fi
mkdir -p config
rm -f config/autoregister.properties
ln -s "${pkgs.writeText "autoregister.properties" cfg.agentConfig}" config/autoregister.properties
${pkgs.git}/bin/git config --global --add http.sslCAinfo /etc/ssl/certs/ca-certificates.crt
${pkgs.jre}/bin/java ${concatStringsSep " " cfg.startupOptions} \
${concatStringsSep " " cfg.extraOptions} \
-jar ${pkgs.gocd-agent}/go-agent/agent-bootstrapper.jar \
-serverUrl ${cfg.goServer}
'';
serviceConfig = {
User = cfg.user;
WorkingDirectory = cfg.workDir;
RestartSec = 30;
Restart = "on-failure";
};
};
};
}

View file

@ -0,0 +1,212 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.gocd-server;
opt = options.services.gocd-server;
in {
options = {
services.gocd-server = {
enable = mkEnableOption "gocd-server";
user = mkOption {
default = "gocd-server";
type = types.str;
description = ''
User the Go.CD server should execute under.
'';
};
group = mkOption {
default = "gocd-server";
type = types.str;
description = ''
If the default user "gocd-server" is configured then this is the primary group of that user.
'';
};
extraGroups = mkOption {
default = [ ];
type = types.listOf types.str;
example = [ "wheel" "docker" ];
description = ''
List of extra groups that the "gocd-server" user should be a part of.
'';
};
listenAddress = mkOption {
default = "0.0.0.0";
example = "localhost";
type = types.str;
description = ''
Specifies the bind address on which the Go.CD server HTTP interface listens.
'';
};
port = mkOption {
default = 8153;
type = types.int;
description = ''
Specifies port number on which the Go.CD server HTTP interface listens.
'';
};
sslPort = mkOption {
default = 8154;
type = types.int;
description = ''
Specifies port number on which the Go.CD server HTTPS interface listens.
'';
};
workDir = mkOption {
default = "/var/lib/go-server";
type = types.str;
description = ''
Specifies the working directory in which the Go.CD server java archive resides.
'';
};
packages = mkOption {
default = [ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ];
defaultText = literalExpression "[ pkgs.stdenv pkgs.jre pkgs.git config.programs.ssh.package pkgs.nix ]";
type = types.listOf types.package;
description = ''
Packages to add to PATH for the Go.CD server's process.
'';
};
initialJavaHeapSize = mkOption {
default = "512m";
type = types.str;
description = ''
Specifies the initial java heap memory size for the Go.CD server's java process.
'';
};
maxJavaHeapMemory = mkOption {
default = "1024m";
type = types.str;
description = ''
Specifies the java maximum heap memory size for the Go.CD server's java process.
'';
};
startupOptions = mkOption {
type = types.listOf types.str;
default = [
"-Xms${cfg.initialJavaHeapSize}"
"-Xmx${cfg.maxJavaHeapMemory}"
"-Dcruise.listen.host=${cfg.listenAddress}"
"-Duser.language=en"
"-Djruby.rack.request.size.threshold.bytes=30000000"
"-Duser.country=US"
"-Dcruise.config.dir=${cfg.workDir}/conf"
"-Dcruise.config.file=${cfg.workDir}/conf/cruise-config.xml"
"-Dcruise.server.port=${toString cfg.port}"
"-Dcruise.server.ssl.port=${toString cfg.sslPort}"
];
defaultText = literalExpression ''
[
"-Xms''${config.${opt.initialJavaHeapSize}}"
"-Xmx''${config.${opt.maxJavaHeapMemory}}"
"-Dcruise.listen.host=''${config.${opt.listenAddress}}"
"-Duser.language=en"
"-Djruby.rack.request.size.threshold.bytes=30000000"
"-Duser.country=US"
"-Dcruise.config.dir=''${config.${opt.workDir}}/conf"
"-Dcruise.config.file=''${config.${opt.workDir}}/conf/cruise-config.xml"
"-Dcruise.server.port=''${toString config.${opt.port}}"
"-Dcruise.server.ssl.port=''${toString config.${opt.sslPort}}"
]
'';
description = ''
Specifies startup command line arguments to pass to Go.CD server
java process.
'';
};
extraOptions = mkOption {
default = [ ];
type = types.listOf types.str;
example = [
"-X debug"
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"
"-verbose:gc"
"-Xloggc:go-server-gc.log"
"-XX:+PrintGCTimeStamps"
"-XX:+PrintTenuringDistribution"
"-XX:+PrintGCDetails"
"-XX:+PrintGC"
];
description = ''
Specifies additional command line arguments to pass to Go.CD server's
java process. Example contains debug and gcLog arguments.
'';
};
environment = mkOption {
default = { };
type = with types; attrsOf str;
description = ''
Additional environment variables to be passed to the gocd-server process.
As a base environment, gocd-server receives NIX_PATH from
<option>environment.sessionVariables</option>, NIX_REMOTE is set to
"daemon".
'';
};
};
};
config = mkIf cfg.enable {
users.groups = optionalAttrs (cfg.group == "gocd-server") {
gocd-server.gid = config.ids.gids.gocd-server;
};
users.users = optionalAttrs (cfg.user == "gocd-server") {
gocd-server = {
description = "gocd-server user";
createHome = true;
home = cfg.workDir;
group = cfg.group;
extraGroups = cfg.extraGroups;
useDefaultShell = true;
uid = config.ids.uids.gocd-server;
};
};
systemd.services.gocd-server = {
description = "GoCD Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment =
let
selectedSessionVars =
lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ])
config.environment.sessionVariables;
in
selectedSessionVars //
{ NIX_REMOTE = "daemon";
} //
cfg.environment;
path = cfg.packages;
script = ''
${pkgs.git}/bin/git config --global --add http.sslCAinfo /etc/ssl/certs/ca-certificates.crt
${pkgs.jre}/bin/java -server ${concatStringsSep " " cfg.startupOptions} \
${concatStringsSep " " cfg.extraOptions} \
-jar ${pkgs.gocd-server}/go-server/go.jar
'';
serviceConfig = {
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.workDir;
};
};
};
}

View file

@ -0,0 +1,61 @@
{ config, lib, pkgs, ...}:
with lib;
let
cfg = config.services.hail;
in {
###### interface
options.services.hail = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enables the Hail Auto Update Service. Hail can automatically deploy artifacts
built by a Hydra Continous Integration server. A common use case is to provide
continous deployment for single services or a full NixOS configuration.'';
};
profile = mkOption {
type = types.str;
default = "hail-profile";
description = "The name of the Nix profile used by Hail.";
};
hydraJobUri = mkOption {
type = types.str;
description = "The URI of the Hydra Job.";
};
netrc = mkOption {
type = types.nullOr types.path;
description = "The netrc file to use when fetching data from Hydra.";
default = null;
};
package = mkOption {
type = types.package;
default = pkgs.haskellPackages.hail;
defaultText = literalExpression "pkgs.haskellPackages.hail";
description = "Hail package to use.";
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.hail = {
description = "Hail Auto Update Service";
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ nix ];
environment = {
HOME = "/var/lib/empty";
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/hail --profile ${cfg.profile} --job-uri ${cfg.hydraJobUri}"
+ lib.optionalString (cfg.netrc != null) " --netrc-file ${cfg.netrc}";
};
};
};
}

View file

@ -0,0 +1,266 @@
/*
This file is for options that NixOS and nix-darwin have in common.
Platform-specific code is in the respective default.nix files.
*/
{ config, lib, options, pkgs, ... }:
let
inherit (lib)
filterAttrs
literalDocBook
literalExpression
mkIf
mkOption
mkRemovedOptionModule
mkRenamedOptionModule
types
;
cfg =
config.services.hercules-ci-agent;
format = pkgs.formats.toml { };
settingsModule = { config, ... }: {
freeformType = format.type;
options = {
apiBaseUrl = mkOption {
description = ''
API base URL that the agent will connect to.
When using Hercules CI Enterprise, set this to the URL where your
Hercules CI server is reachable.
'';
type = types.str;
default = "https://hercules-ci.com";
};
baseDirectory = mkOption {
type = types.path;
default = "/var/lib/hercules-ci-agent";
description = ''
State directory (secrets, work directory, etc) for agent
'';
};
concurrentTasks = mkOption {
description = ''
Number of tasks to perform simultaneously.
A task is a single derivation build, an evaluation or an effect run.
At minimum, you need 2 concurrent tasks for <literal>x86_64-linux</literal>
in your cluster, to allow for import from derivation.
<literal>concurrentTasks</literal> can be around the CPU core count or lower if memory is
the bottleneck.
The optimal value depends on the resource consumption characteristics of your workload,
including memory usage and in-task parallelism. This is typically determined empirically.
When scaling, it is generally better to have a double-size machine than two machines,
because each split of resources causes inefficiencies; particularly with regards
to build latency because of extra downloads.
'';
type = types.either types.ints.positive (types.enum [ "auto" ]);
default = "auto";
};
labels = mkOption {
description = ''
A key-value map of user data.
This data will be available to organization members in the dashboard and API.
The values can be of any TOML type that corresponds to a JSON type, but arrays
can not contain tables/objects due to limitations of the TOML library. Values
involving arrays of non-primitive types may not be representable currently.
'';
type = format.type;
defaultText = literalExpression ''
{
agent.source = "..."; # One of "nixpkgs", "flake", "override"
lib.version = "...";
pkgs.version = "...";
}
'';
};
workDirectory = mkOption {
description = ''
The directory in which temporary subdirectories are created for task state. This includes sources for Nix evaluation.
'';
type = types.path;
default = config.baseDirectory + "/work";
defaultText = literalExpression ''baseDirectory + "/work"'';
};
staticSecretsDirectory = mkOption {
description = ''
This is the default directory to look for statically configured secrets like <literal>cluster-join-token.key</literal>.
See also <literal>clusterJoinTokenPath</literal> and <literal>binaryCachesPath</literal> for fine-grained configuration.
'';
type = types.path;
default = config.baseDirectory + "/secrets";
defaultText = literalExpression ''baseDirectory + "/secrets"'';
};
clusterJoinTokenPath = mkOption {
description = ''
Location of the cluster-join-token.key file.
You can retrieve the contents of the file when creating a new agent via
<link xlink:href="https://hercules-ci.com/dashboard">https://hercules-ci.com/dashboard</link>.
As this value is confidential, it should not be in the store, but
installed using other means, such as agenix, NixOps
<literal>deployment.keys</literal>, or manual installation.
The contents of the file are used for authentication between the agent and the API.
'';
type = types.path;
default = config.staticSecretsDirectory + "/cluster-join-token.key";
defaultText = literalExpression ''staticSecretsDirectory + "/cluster-join-token.key"'';
};
binaryCachesPath = mkOption {
description = ''
Path to a JSON file containing binary cache secret keys.
As these values are confidential, they should not be in the store, but
copied over using other means, such as agenix, NixOps
<literal>deployment.keys</literal>, or manual installation.
The format is described on <link xlink:href="https://docs.hercules-ci.com/hercules-ci-agent/binary-caches-json/">https://docs.hercules-ci.com/hercules-ci-agent/binary-caches-json/</link>.
'';
type = types.path;
default = config.staticSecretsDirectory + "/binary-caches.json";
defaultText = literalExpression ''staticSecretsDirectory + "/binary-caches.json"'';
};
secretsJsonPath = mkOption {
description = ''
Path to a JSON file containing secrets for effects.
As these values are confidential, they should not be in the store, but
copied over using other means, such as agenix, NixOps
<literal>deployment.keys</literal>, or manual installation.
The format is described on <link xlink:href="https://docs.hercules-ci.com/hercules-ci-agent/secrets-json/">https://docs.hercules-ci.com/hercules-ci-agent/secrets-json/</link>.
'';
type = types.path;
default = config.staticSecretsDirectory + "/secrets.json";
defaultText = literalExpression ''staticSecretsDirectory + "/secrets.json"'';
};
};
};
# TODO (roberth, >=2022) remove
checkNix =
if !cfg.checkNix
then ""
else if lib.versionAtLeast config.nix.package.version "2.3.10"
then ""
else
pkgs.stdenv.mkDerivation {
name = "hercules-ci-check-system-nix-src";
inherit (config.nix.package) src patches;
dontConfigure = true;
buildPhase = ''
echo "Checking in-memory pathInfoCache expiry"
if ! grep 'PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
cat 1>&2 <<EOF
You are deploying Hercules CI Agent on a system with an incompatible
nix-daemon. Please make sure nix.package is set to a Nix version of at
least 2.3.10 or a master version more recent than Mar 12, 2020.
EOF
exit 1
fi
'';
installPhase = "touch $out";
};
in
{
imports = [
(mkRenamedOptionModule [ "services" "hercules-ci-agent" "extraOptions" ] [ "services" "hercules-ci-agent" "settings" ])
(mkRenamedOptionModule [ "services" "hercules-ci-agent" "baseDirectory" ] [ "services" "hercules-ci-agent" "settings" "baseDirectory" ])
(mkRenamedOptionModule [ "services" "hercules-ci-agent" "concurrentTasks" ] [ "services" "hercules-ci-agent" "settings" "concurrentTasks" ])
(mkRemovedOptionModule [ "services" "hercules-ci-agent" "patchNix" ] "Nix versions packaged in this version of Nixpkgs don't need a patched nix-daemon to work correctly in Hercules CI Agent clusters.")
];
options.services.hercules-ci-agent = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable to run Hercules CI Agent as a system service.
<link xlink:href="https://hercules-ci.com">Hercules CI</link> is a
continuous integation service that is centered around Nix.
Support is available at <link xlink:href="mailto:help@hercules-ci.com">help@hercules-ci.com</link>.
'';
};
checkNix = mkOption {
type = types.bool;
default = true;
description = ''
Whether to make sure that the system's Nix (nix-daemon) is compatible.
If you set this to false, please keep up with the change log.
'';
};
package = mkOption {
description = ''
Package containing the bin/hercules-ci-agent executable.
'';
type = types.package;
default = pkgs.hercules-ci-agent;
defaultText = literalExpression "pkgs.hercules-ci-agent";
};
settings = mkOption {
description = ''
These settings are written to the <literal>agent.toml</literal> file.
Not all settings are listed as options, can be set nonetheless.
For the exhaustive list of settings, see <link xlink:href="https://docs.hercules-ci.com/hercules-ci/reference/agent-config/"/>.
'';
type = types.submoduleWith { modules = [ settingsModule ]; };
};
/*
Internal and/or computed values.
These are written as options instead of let binding to allow sharing with
default.nix on both NixOS and nix-darwin.
*/
tomlFile = mkOption {
type = types.path;
internal = true;
defaultText = literalDocBook "generated <literal>hercules-ci-agent.toml</literal>";
description = ''
The fully assembled config file.
'';
};
};
config = mkIf cfg.enable {
nix.extraOptions = lib.addContextFrom checkNix ''
# A store path that was missing at first may well have finished building,
# even shortly after the previous lookup. This *also* applies to the daemon.
narinfo-cache-negative-ttl = 0
'';
services.hercules-ci-agent = {
tomlFile =
format.generate "hercules-ci-agent.toml" cfg.settings;
settings.labels = {
agent.source =
if options.services.hercules-ci-agent.package.highestPrio == (lib.modules.mkOptionDefault { }).priority
then "nixpkgs"
else lib.mkOptionDefault "override";
pkgs.version = pkgs.lib.version;
lib.version = lib.version;
};
};
};
}

View file

@ -0,0 +1,101 @@
/*
This file is for NixOS-specific options and configs.
Code that is shared with nix-darwin goes in common.nix.
*/
{ pkgs, config, lib, ... }:
let
inherit (lib) mkIf mkDefault;
cfg = config.services.hercules-ci-agent;
command = "${cfg.package}/bin/hercules-ci-agent --config ${cfg.tomlFile}";
testCommand = "${command} --test-configuration";
in
{
imports = [
./common.nix
(lib.mkRenamedOptionModule [ "services" "hercules-ci-agent" "user" ] [ "systemd" "services" "hercules-ci-agent" "serviceConfig" "User" ])
];
config = mkIf cfg.enable {
systemd.services.hercules-ci-agent = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = [ config.nix.package ];
startLimitBurst = 30 * 1000000; # practically infinite
serviceConfig = {
User = "hercules-ci-agent";
ExecStart = command;
ExecStartPre = testCommand;
Restart = "on-failure";
RestartSec = 120;
};
};
# Changes in the secrets do not affect the unit in any way that would cause
# a restart, which is currently necessary to reload the secrets.
systemd.paths.hercules-ci-agent-restart-files = {
wantedBy = [ "hercules-ci-agent.service" ];
pathConfig = {
Unit = "hercules-ci-agent-restarter.service";
PathChanged = [ cfg.settings.clusterJoinTokenPath cfg.settings.binaryCachesPath ];
};
};
systemd.services.hercules-ci-agent-restarter = {
serviceConfig.Type = "oneshot";
script = ''
# Wait a bit, with the effect of bundling up file changes into a single
# run of this script and hopefully a single restart.
sleep 10
if systemctl is-active --quiet hercules-ci-agent.service; then
if ${testCommand}; then
systemctl restart hercules-ci-agent.service
else
echo 1>&2 "WARNING: Not restarting agent because config is not valid at this time."
fi
else
echo 1>&2 "Not restarting hercules-ci-agent despite config file update, because it is not already active."
fi
'';
};
# Trusted user allows simplified configuration and better performance
# when operating in a cluster.
nix.settings.trusted-users = [ config.systemd.services.hercules-ci-agent.serviceConfig.User ];
services.hercules-ci-agent = {
settings = {
nixUserIsTrusted = true;
labels =
let
mkIfNotNull = x: mkIf (x != null) x;
in
{
nixos.configurationRevision = mkIfNotNull config.system.configurationRevision;
nixos.release = config.system.nixos.release;
nixos.label = mkIfNotNull config.system.nixos.label;
nixos.codeName = config.system.nixos.codeName;
nixos.tags = config.system.nixos.tags;
nixos.systemName = mkIfNotNull config.system.name;
};
};
};
users.users.hercules-ci-agent = {
home = cfg.settings.baseDirectory;
createHome = true;
group = "hercules-ci-agent";
description = "Hercules CI Agent system user";
isSystemUser = true;
};
users.groups.hercules-ci-agent = { };
};
meta.maintainers = [ lib.maintainers.roberth ];
}

View file

@ -0,0 +1,505 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.hydra;
baseDir = "/var/lib/hydra";
hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
hydraEnv =
{ HYDRA_DBI = cfg.dbi;
HYDRA_CONFIG = "${baseDir}/hydra.conf";
HYDRA_DATA = "${baseDir}";
};
env =
{ NIX_REMOTE = "daemon";
SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; # Remove in 16.03
PGPASSFILE = "${baseDir}/pgpass";
NIX_REMOTE_SYSTEMS = concatStringsSep ":" cfg.buildMachinesFiles;
} // optionalAttrs (cfg.smtpHost != null) {
EMAIL_SENDER_TRANSPORT = "SMTP";
EMAIL_SENDER_TRANSPORT_host = cfg.smtpHost;
} // hydraEnv // cfg.extraEnv;
serverEnv = env //
{ HYDRA_TRACKER = cfg.tracker;
XDG_CACHE_HOME = "${baseDir}/www/.cache";
COLUMNS = "80";
PGPASSFILE = "${baseDir}/pgpass-www"; # grrr
} // (optionalAttrs cfg.debugServer { DBIC_TRACE = "1"; });
localDB = "dbi:Pg:dbname=hydra;user=hydra;";
haveLocalDB = cfg.dbi == localDB;
hydra-package =
let
makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv);
in pkgs.buildEnv rec {
name = "hydra-env";
buildInputs = [ pkgs.makeWrapper ];
paths = [ cfg.package ];
postBuild = ''
if [ -L "$out/bin" ]; then
unlink "$out/bin"
fi
mkdir -p "$out/bin"
for path in ${concatStringsSep " " paths}; do
if [ -d "$path/bin" ]; then
cd "$path/bin"
for prg in *; do
if [ -f "$prg" ]; then
rm -f "$out/bin/$prg"
if [ -x "$prg" ]; then
makeWrapper "$path/bin/$prg" "$out/bin/$prg" ${makeWrapperArgs}
fi
fi
done
fi
done
'';
};
in
{
###### interface
options = {
services.hydra = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to run Hydra services.
'';
};
dbi = mkOption {
type = types.str;
default = localDB;
example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
description = ''
The DBI string for Hydra database connection.
NOTE: Attempts to set `application_name` will be overridden by
`hydra-TYPE` (where TYPE is e.g. `evaluator`, `queue-runner`,
etc.) in all hydra services to more easily distinguish where
queries are coming from.
'';
};
package = mkOption {
type = types.package;
default = pkgs.hydra_unstable;
defaultText = literalExpression "pkgs.hydra_unstable";
description = "The Hydra package.";
};
hydraURL = mkOption {
type = types.str;
description = ''
The base URL for the Hydra webserver instance. Used for links in emails.
'';
};
listenHost = mkOption {
type = types.str;
default = "*";
example = "localhost";
description = ''
The hostname or address to listen on or <literal>*</literal> to listen
on all interfaces.
'';
};
port = mkOption {
type = types.int;
default = 3000;
description = ''
TCP port the web server should listen to.
'';
};
minimumDiskFree = mkOption {
type = types.int;
default = 0;
description = ''
Threshold of minimum disk space (GiB) to determine if the queue runner should run or not.
'';
};
minimumDiskFreeEvaluator = mkOption {
type = types.int;
default = 0;
description = ''
Threshold of minimum disk space (GiB) to determine if the evaluator should run or not.
'';
};
notificationSender = mkOption {
type = types.str;
description = ''
Sender email address used for email notifications.
'';
};
smtpHost = mkOption {
type = types.nullOr types.str;
default = null;
example = "localhost";
description = ''
Hostname of the SMTP server to use to send email.
'';
};
tracker = mkOption {
type = types.str;
default = "";
description = ''
Piece of HTML that is included on all pages.
'';
};
logo = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a file containing the logo of your Hydra instance.
'';
};
debugServer = mkOption {
type = types.bool;
default = false;
description = "Whether to run the server in debug mode.";
};
extraConfig = mkOption {
type = types.lines;
description = "Extra lines for the Hydra configuration.";
};
extraEnv = mkOption {
type = types.attrsOf types.str;
default = {};
description = "Extra environment variables for Hydra.";
};
gcRootsDir = mkOption {
type = types.path;
default = "/nix/var/nix/gcroots/hydra";
description = "Directory that holds Hydra garbage collector roots.";
};
buildMachinesFiles = mkOption {
type = types.listOf types.path;
default = optional (config.nix.buildMachines != []) "/etc/nix/machines";
defaultText = literalExpression ''optional (config.nix.buildMachines != []) "/etc/nix/machines"'';
example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
description = "List of files containing build machines.";
};
useSubstitutes = mkOption {
type = types.bool;
default = false;
description = ''
Whether to use binary caches for downloading store paths. Note that
binary substitutions trigger (a potentially large number of) additional
HTTP requests that slow down the queue monitor thread significantly.
Also, this Hydra instance will serve those downloaded store paths to
its users with its own signature attached as if it had built them
itself, so don't enable this feature unless your active binary caches
are absolute trustworthy.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
users.groups.hydra = {
gid = config.ids.gids.hydra;
};
users.users.hydra =
{ description = "Hydra";
group = "hydra";
# We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
home = baseDir;
useDefaultShell = true;
uid = config.ids.uids.hydra;
};
users.users.hydra-queue-runner =
{ description = "Hydra queue runner";
group = "hydra";
useDefaultShell = true;
home = "${baseDir}/queue-runner"; # really only to keep SSH happy
uid = config.ids.uids.hydra-queue-runner;
};
users.users.hydra-www =
{ description = "Hydra web server";
group = "hydra";
useDefaultShell = true;
uid = config.ids.uids.hydra-www;
};
services.hydra.extraConfig =
''
using_frontend_proxy = 1
base_uri = ${cfg.hydraURL}
notification_sender = ${cfg.notificationSender}
max_servers = 25
${optionalString (cfg.logo != null) ''
hydra_logo = ${cfg.logo}
''}
gc_roots_dir = ${cfg.gcRootsDir}
use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
'';
environment.systemPackages = [ hydra-package ];
environment.variables = hydraEnv;
nix.settings = mkMerge [
{
keep-outputs = true;
keep-derivations = true;
trusted-users = [ "hydra-queue-runner" ];
}
(mkIf (versionOlder (getVersion config.nix.package.out) "2.4pre")
{
# The default (`true') slows Nix down a lot since the build farm
# has so many GC roots.
gc-check-reachability = false;
}
)
];
systemd.services.hydra-init =
{ wantedBy = [ "multi-user.target" ];
requires = optional haveLocalDB "postgresql.service";
after = optional haveLocalDB "postgresql.service";
environment = env // {
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
};
preStart = ''
mkdir -p ${baseDir}
chown hydra:hydra ${baseDir}
chmod 0750 ${baseDir}
ln -sf ${hydraConf} ${baseDir}/hydra.conf
mkdir -m 0700 -p ${baseDir}/www
chown hydra-www:hydra ${baseDir}/www
mkdir -m 0700 -p ${baseDir}/queue-runner
mkdir -m 0750 -p ${baseDir}/build-logs
mkdir -m 0750 -p ${baseDir}/runcommand-logs
chown hydra-queue-runner.hydra \
${baseDir}/queue-runner \
${baseDir}/build-logs \
${baseDir}/runcommand-logs
${optionalString haveLocalDB ''
if ! [ -e ${baseDir}/.db-created ]; then
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -O hydra hydra
touch ${baseDir}/.db-created
fi
echo "create extension if not exists pg_trgm" | ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
''}
if [ ! -e ${cfg.gcRootsDir} ]; then
# Move legacy roots directory.
if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then
mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir}
fi
mkdir -p ${cfg.gcRootsDir}
fi
# Move legacy hydra-www roots.
if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then
find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \
| xargs -r mv -f -t ${cfg.gcRootsDir}/
rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
fi
chown hydra:hydra ${cfg.gcRootsDir}
chmod 2775 ${cfg.gcRootsDir}
'';
serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
serviceConfig.PermissionsStartOnly = true;
serviceConfig.User = "hydra";
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
};
systemd.services.hydra-server =
{ wantedBy = [ "multi-user.target" ];
requires = [ "hydra-init.service" ];
after = [ "hydra-init.service" ];
environment = serverEnv // {
HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
};
restartTriggers = [ hydraConf ];
serviceConfig =
{ ExecStart =
"@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
+ "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 "
+ "--max_requests 100 ${optionalString cfg.debugServer "-d"}";
User = "hydra-www";
PermissionsStartOnly = true;
Restart = "always";
};
};
systemd.services.hydra-queue-runner =
{ wantedBy = [ "multi-user.target" ];
requires = [ "hydra-init.service" ];
after = [ "hydra-init.service" "network.target" ];
path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
restartTriggers = [ hydraConf ];
environment = env // {
PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
IN_SYSTEMD = "1"; # to get log severity levels
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner";
};
serviceConfig =
{ ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock";
User = "hydra-queue-runner";
Restart = "always";
# Ensure we can get core dumps.
LimitCORE = "infinity";
WorkingDirectory = "${baseDir}/queue-runner";
};
};
systemd.services.hydra-evaluator =
{ wantedBy = [ "multi-user.target" ];
requires = [ "hydra-init.service" ];
after = [ "hydra-init.service" "network.target" ];
path = with pkgs; [ hydra-package nettools jq ];
restartTriggers = [ hydraConf ];
environment = env // {
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
};
serviceConfig =
{ ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
User = "hydra";
Restart = "always";
WorkingDirectory = baseDir;
};
};
systemd.services.hydra-update-gc-roots =
{ requires = [ "hydra-init.service" ];
after = [ "hydra-init.service" ];
environment = env // {
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots";
};
serviceConfig =
{ ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
User = "hydra";
};
startAt = "2,14:15";
};
systemd.services.hydra-send-stats =
{ wantedBy = [ "multi-user.target" ];
after = [ "hydra-init.service" ];
environment = env // {
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats";
};
serviceConfig =
{ ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
User = "hydra";
};
};
systemd.services.hydra-notify =
{ wantedBy = [ "multi-user.target" ];
requires = [ "hydra-init.service" ];
after = [ "hydra-init.service" ];
restartTriggers = [ hydraConf ];
environment = env // {
PGPASSFILE = "${baseDir}/pgpass-queue-runner";
HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
};
serviceConfig =
{ ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
# FIXME: run this under a less privileged user?
User = "hydra-queue-runner";
Restart = "always";
RestartSec = 5;
};
};
# If there is less than a certain amount of free disk space, stop
# the queue/evaluator to prevent builds from failing or aborting.
systemd.services.hydra-check-space =
{ script =
''
if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
echo "stopping Hydra queue runner due to lack of free space..."
systemctl stop hydra-queue-runner
fi
if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
echo "stopping Hydra evaluator due to lack of free space..."
systemctl stop hydra-evaluator
fi
'';
startAt = "*:0/5";
};
# Periodically compress build logs. The queue runner compresses
# logs automatically after a step finishes, but this doesn't work
# if the queue runner is stopped prematurely.
systemd.services.hydra-compress-logs =
{ path = [ pkgs.bzip2 ];
script =
''
find /var/lib/hydra/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f
'';
startAt = "Sun 01:45";
};
services.postgresql.enable = mkIf haveLocalDB true;
services.postgresql.identMap = optionalString haveLocalDB
''
hydra-users hydra hydra
hydra-users hydra-queue-runner hydra
hydra-users hydra-www hydra
hydra-users root hydra
# The postgres user is used to create the pg_trgm extension for the hydra database
hydra-users postgres postgres
'';
services.postgresql.authentication = optionalString haveLocalDB
''
local hydra all ident map=hydra-users
'';
};
}

View file

@ -0,0 +1,249 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.jenkins;
jenkinsUrl = "http://${cfg.listenAddress}:${toString cfg.port}${cfg.prefix}";
in {
options = {
services.jenkins = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the jenkins continuous integration server.
'';
};
user = mkOption {
default = "jenkins";
type = types.str;
description = ''
User the jenkins server should execute under.
'';
};
group = mkOption {
default = "jenkins";
type = types.str;
description = ''
If the default user "jenkins" is configured then this is the primary
group of that user.
'';
};
extraGroups = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "wheel" "dialout" ];
description = ''
List of extra groups that the "jenkins" user should be a part of.
'';
};
home = mkOption {
default = "/var/lib/jenkins";
type = types.path;
description = ''
The path to use as JENKINS_HOME. If the default user "jenkins" is configured then
this is the home of the "jenkins" user.
'';
};
listenAddress = mkOption {
default = "0.0.0.0";
example = "localhost";
type = types.str;
description = ''
Specifies the bind address on which the jenkins HTTP interface listens.
The default is the wildcard address.
'';
};
port = mkOption {
default = 8080;
type = types.port;
description = ''
Specifies port number on which the jenkins HTTP interface listens.
The default is 8080.
'';
};
prefix = mkOption {
default = "";
example = "/jenkins";
type = types.str;
description = ''
Specifies a urlPrefix to use with jenkins.
If the example /jenkins is given, the jenkins server will be
accessible using localhost:8080/jenkins.
'';
};
package = mkOption {
default = pkgs.jenkins;
defaultText = literalExpression "pkgs.jenkins";
type = types.package;
description = "Jenkins package to use.";
};
packages = mkOption {
default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ];
defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]";
type = types.listOf types.package;
description = ''
Packages to add to PATH for the jenkins process.
'';
};
environment = mkOption {
default = { };
type = with types; attrsOf str;
description = ''
Additional environment variables to be passed to the jenkins process.
As a base environment, jenkins receives NIX_PATH from
<option>environment.sessionVariables</option>, NIX_REMOTE is set to
"daemon" and JENKINS_HOME is set to the value of
<option>services.jenkins.home</option>.
This option has precedence and can be used to override those
mentioned variables.
'';
};
plugins = mkOption {
default = null;
type = types.nullOr (types.attrsOf types.package);
description = ''
A set of plugins to activate. Note that this will completely
remove and replace any previously installed plugins. If you
have manually-installed plugins that you want to keep while
using this module, set this option to
<literal>null</literal>. You can generate this set with a
tool such as <literal>jenkinsPlugins2nix</literal>.
'';
example = literalExpression ''
import path/to/jenkinsPlugins2nix-generated-plugins.nix { inherit (pkgs) fetchurl stdenv; }
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--debug=9" ];
description = ''
Additional command line arguments to pass to Jenkins.
'';
};
extraJavaOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "-Xmx80m" ];
description = ''
Additional command line arguments to pass to the Java run time (as opposed to Jenkins).
'';
};
withCLI = mkOption {
type = types.bool;
default = false;
description = ''
Whether to make the CLI available.
More info about the CLI available at
<link xlink:href="https://www.jenkins.io/doc/book/managing/cli">
https://www.jenkins.io/doc/book/managing/cli</link> .
'';
};
};
};
config = mkIf cfg.enable {
environment = {
# server references the dejavu fonts
systemPackages = [
pkgs.dejavu_fonts
] ++ optional cfg.withCLI cfg.package;
variables = {}
// optionalAttrs cfg.withCLI {
# Make it more convenient to use the `jenkins-cli`.
JENKINS_URL = jenkinsUrl;
};
};
users.groups = optionalAttrs (cfg.group == "jenkins") {
jenkins.gid = config.ids.gids.jenkins;
};
users.users = optionalAttrs (cfg.user == "jenkins") {
jenkins = {
description = "jenkins user";
createHome = true;
home = cfg.home;
group = cfg.group;
extraGroups = cfg.extraGroups;
useDefaultShell = true;
uid = config.ids.uids.jenkins;
};
};
systemd.services.jenkins = {
description = "Jenkins Continuous Integration Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment =
let
selectedSessionVars =
lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ])
config.environment.sessionVariables;
in
selectedSessionVars //
{ JENKINS_HOME = cfg.home;
NIX_REMOTE = "daemon";
} //
cfg.environment;
path = cfg.packages;
# Force .war (re)extraction, or else we might run stale Jenkins.
preStart =
let replacePlugins =
if cfg.plugins == null
then ""
else
let pluginCmds = lib.attrsets.mapAttrsToList
(n: v: "cp ${v} ${cfg.home}/plugins/${n}.jpi")
cfg.plugins;
in ''
rm -r ${cfg.home}/plugins || true
mkdir -p ${cfg.home}/plugins
${lib.strings.concatStringsSep "\n" pluginCmds}
'';
in ''
rm -rf ${cfg.home}/war
${replacePlugins}
'';
# For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript
script = ''
${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
--httpPort=${toString cfg.port} \
--prefix=${cfg.prefix} \
-Djava.awt.headless=true \
${concatStringsSep " " cfg.extraOptions}
'';
postStart = ''
until [[ $(${pkgs.curl.bin}/bin/curl -L -s --head -w '\n%{http_code}' ${jenkinsUrl} | tail -n1) =~ ^(200|403)$ ]]; do
sleep 1
done
'';
serviceConfig = {
User = cfg.user;
};
};
};
}

View file

@ -0,0 +1,241 @@
{ config, lib, pkgs, ... }:
with lib;
let
jenkinsCfg = config.services.jenkins;
cfg = config.services.jenkins.jobBuilder;
in {
options = {
services.jenkins.jobBuilder = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether or not to enable the Jenkins Job Builder (JJB) service. It
allows defining jobs for Jenkins in a declarative manner.
Jobs managed through the Jenkins WebUI (or by other means) are left
unchanged.
Note that it really is declarative configuration; if you remove a
previously defined job, the corresponding job directory will be
deleted.
Please see the Jenkins Job Builder documentation for more info:
<link xlink:href="http://docs.openstack.org/infra/jenkins-job-builder/">
http://docs.openstack.org/infra/jenkins-job-builder/</link>
'';
};
accessUser = mkOption {
default = "";
type = types.str;
description = ''
User id in Jenkins used to reload config.
'';
};
accessToken = mkOption {
default = "";
type = types.str;
description = ''
User token in Jenkins used to reload config.
WARNING: This token will be world readable in the Nix store. To keep
it secret, use the <option>accessTokenFile</option> option instead.
'';
};
accessTokenFile = mkOption {
default = "";
type = types.str;
example = "/run/keys/jenkins-job-builder-access-token";
description = ''
File containing the API token for the <option>accessUser</option>
user.
'';
};
yamlJobs = mkOption {
default = "";
type = types.lines;
example = ''
- job:
name: jenkins-job-test-1
builders:
- shell: echo 'Hello world!'
'';
description = ''
Job descriptions for Jenkins Job Builder in YAML format.
'';
};
jsonJobs = mkOption {
default = [ ];
type = types.listOf types.str;
example = literalExpression ''
[
'''
[ { "job":
{ "name": "jenkins-job-test-2",
"builders": [ "shell": "echo 'Hello world!'" ]
}
}
]
'''
]
'';
description = ''
Job descriptions for Jenkins Job Builder in JSON format.
'';
};
nixJobs = mkOption {
default = [ ];
type = types.listOf types.attrs;
example = literalExpression ''
[ { job =
{ name = "jenkins-job-test-3";
builders = [
{ shell = "echo 'Hello world!'"; }
];
};
}
]
'';
description = ''
Job descriptions for Jenkins Job Builder in Nix format.
This is a trivial wrapper around jsonJobs, using builtins.toJSON
behind the scene.
'';
};
};
};
config = mkIf (jenkinsCfg.enable && cfg.enable) {
assertions = [
{ assertion =
if cfg.accessUser != ""
then (cfg.accessToken != "" && cfg.accessTokenFile == "") ||
(cfg.accessToken == "" && cfg.accessTokenFile != "")
else true;
message = ''
One of accessToken and accessTokenFile options must be non-empty
strings, but not both. Current values:
services.jenkins.jobBuilder.accessToken = "${cfg.accessToken}"
services.jenkins.jobBuilder.accessTokenFile = "${cfg.accessTokenFile}"
'';
}
];
systemd.services.jenkins-job-builder = {
description = "Jenkins Job Builder Service";
# JJB can run either before or after jenkins. We chose after, so we can
# always use curl to notify (running) jenkins to reload its config.
after = [ "jenkins.service" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ jenkins-job-builder curl ];
# Q: Why manipulate files directly instead of using "jenkins-jobs upload [...]"?
# A: Because this module is for administering a local jenkins install,
# and using local file copy allows us to not worry about
# authentication.
script =
let
yamlJobsFile = builtins.toFile "jobs.yaml" cfg.yamlJobs;
jsonJobsFiles =
map (x: (builtins.toFile "jobs.json" x))
(cfg.jsonJobs ++ [(builtins.toJSON cfg.nixJobs)]);
jobBuilderOutputDir = "/run/jenkins-job-builder/output";
# Stamp file is placed in $JENKINS_HOME/jobs/$JOB_NAME/ to indicate
# ownership. Enables tracking and removal of stale jobs.
ownerStamp = ".config-xml-managed-by-nixos-jenkins-job-builder";
reloadScript = ''
echo "Asking Jenkins to reload config"
curl_opts="--silent --fail --show-error"
access_token=${if cfg.accessTokenFile != ""
then "$(cat '${cfg.accessTokenFile}')"
else cfg.accessToken}
jenkins_url="http://${cfg.accessUser}:$access_token@${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
crumb=$(curl $curl_opts "$jenkins_url"'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)')
curl $curl_opts -X POST -H "$crumb" "$jenkins_url"/reload
'';
in
''
joinByString()
{
local separator="$1"
shift
local first="$1"
shift
printf "%s" "$first" "''${@/#/$separator}"
}
# Map a relative directory path in the output from
# jenkins-job-builder (jobname) to the layout expected by jenkins:
# each directory level gets prepended "jobs/".
getJenkinsJobDir()
{
IFS='/' read -ra input_dirs <<< "$1"
printf "jobs/"
joinByString "/jobs/" "''${input_dirs[@]}"
}
# The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
getJobname()
{
IFS='/' read -ra input_dirs <<< "$1"
local i=0
local nelem=''${#input_dirs[@]}
for e in "''${input_dirs[@]}"; do
if [ $((i % 2)) -eq 1 ]; then
printf "$e"
if [ $i -lt $(( nelem - 1 )) ]; then
printf "/"
fi
fi
i=$((i + 1))
done
}
rm -rf ${jobBuilderOutputDir}
cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs
rm -f "$cur_decl_jobs"
# Create / update jobs
mkdir -p ${jobBuilderOutputDir}
for inputFile in ${yamlJobsFile} ${concatStringsSep " " jsonJobsFiles}; do
HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test --config-xml -o "${jobBuilderOutputDir}" "$inputFile"
done
find "${jobBuilderOutputDir}" -type f -name config.xml | while read -r f; do echo "$(dirname "$f")"; done | sort | while read -r dir; do
jobname="$(realpath --relative-to="${jobBuilderOutputDir}" "$dir")"
jenkinsjobname=$(getJenkinsJobDir "$jobname")
jenkinsjobdir="${jenkinsCfg.home}/$jenkinsjobname"
echo "Creating / updating job \"$jobname\""
mkdir -p "$jenkinsjobdir"
touch "$jenkinsjobdir/${ownerStamp}"
cp "$dir"/config.xml "$jenkinsjobdir/config.xml"
echo "$jenkinsjobname" >> "$cur_decl_jobs"
done
# Remove stale jobs
find "${jenkinsCfg.home}" -type f -name "${ownerStamp}" | while read -r f; do echo "$(dirname "$f")"; done | sort --reverse | while read -r dir; do
jenkinsjobname="$(realpath --relative-to="${jenkinsCfg.home}" "$dir")"
grep --quiet --line-regexp "$jenkinsjobname" "$cur_decl_jobs" 2>/dev/null && continue
jobname=$(getJobname "$jenkinsjobname")
echo "Deleting stale job \"$jobname\""
jobdir="${jenkinsCfg.home}/$jenkinsjobname"
rm -rf "$jobdir"
done
'' + (if cfg.accessUser != "" then reloadScript else "");
serviceConfig = {
User = jenkinsCfg.user;
RuntimeDirectory = "jenkins-job-builder";
};
};
};
}

View file

@ -0,0 +1,82 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.jenkinsSlave;
masterCfg = config.services.jenkins;
in {
options = {
services.jenkinsSlave = {
# todo:
# * assure the profile of the jenkins user has a JRE and any specified packages. This would
# enable ssh slaves.
# * Optionally configure the node as a jenkins ad-hoc slave. This would imply configuration
# properties for the master node.
enable = mkOption {
type = types.bool;
default = false;
description = ''
If true the system will be configured to work as a jenkins slave.
If the system is also configured to work as a jenkins master then this has no effect.
In progress: Currently only assures the jenkins user is configured.
'';
};
user = mkOption {
default = "jenkins";
type = types.str;
description = ''
User the jenkins slave agent should execute under.
'';
};
group = mkOption {
default = "jenkins";
type = types.str;
description = ''
If the default slave agent user "jenkins" is configured then this is
the primary group of that user.
'';
};
home = mkOption {
default = "/var/lib/jenkins";
type = types.path;
description = ''
The path to use as JENKINS_HOME. If the default user "jenkins" is configured then
this is the home of the "jenkins" user.
'';
};
javaPackage = mkOption {
default = pkgs.jdk;
defaultText = literalExpression "pkgs.jdk";
description = ''
Java package to install.
'';
type = types.package;
};
};
};
config = mkIf (cfg.enable && !masterCfg.enable) {
users.groups = optionalAttrs (cfg.group == "jenkins") {
jenkins.gid = config.ids.gids.jenkins;
};
users.users = optionalAttrs (cfg.user == "jenkins") {
jenkins = {
description = "jenkins user";
createHome = true;
home = cfg.home;
group = cfg.group;
useDefaultShell = true;
uid = config.ids.uids.jenkins;
};
};
programs.java = {
enable = true;
package = cfg.javaPackage;
};
};
}

View file

@ -0,0 +1,156 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.aerospike;
aerospikeConf = pkgs.writeText "aerospike.conf" ''
# This stanza must come first.
service {
user aerospike
group aerospike
paxos-single-replica-limit 1 # Number of nodes where the replica count is automatically reduced to 1.
proto-fd-max 15000
work-directory ${cfg.workDir}
}
logging {
console {
context any info
}
}
mod-lua {
system-path ${cfg.package}/share/udf/lua
user-path ${cfg.workDir}/udf/lua
}
network {
${cfg.networkConfig}
}
${cfg.extraConfig}
'';
in
{
###### interface
options = {
services.aerospike = {
enable = mkEnableOption "Aerospike server";
package = mkOption {
default = pkgs.aerospike;
defaultText = literalExpression "pkgs.aerospike";
type = types.package;
description = "Which Aerospike derivation to use";
};
workDir = mkOption {
type = types.str;
default = "/var/lib/aerospike";
description = "Location where Aerospike stores its files";
};
networkConfig = mkOption {
type = types.lines;
default = ''
service {
address any
port 3000
}
heartbeat {
address any
mode mesh
port 3002
interval 150
timeout 10
}
fabric {
address any
port 3001
}
info {
address any
port 3003
}
'';
description = "network section of configuration file";
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
namespace test {
replication-factor 2
memory-size 4G
default-ttl 30d
storage-engine memory
}
'';
description = "Extra configuration";
};
};
};
###### implementation
config = mkIf config.services.aerospike.enable {
users.users.aerospike = {
name = "aerospike";
group = "aerospike";
uid = config.ids.uids.aerospike;
description = "Aerospike server user";
};
users.groups.aerospike.gid = config.ids.gids.aerospike;
systemd.services.aerospike = rec {
description = "Aerospike server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/asd --fgdaemon --config-file ${aerospikeConf}";
User = "aerospike";
Group = "aerospike";
LimitNOFILE = 100000;
PermissionsStartOnly = true;
};
preStart = ''
if [ $(echo "$(${pkgs.procps}/bin/sysctl -n kernel.shmall) < 4294967296" | ${pkgs.bc}/bin/bc) == "1" ]; then
echo "kernel.shmall too low, setting to 4G pages"
${pkgs.procps}/bin/sysctl -w kernel.shmall=4294967296
fi
if [ $(echo "$(${pkgs.procps}/bin/sysctl -n kernel.shmmax) < 1073741824" | ${pkgs.bc}/bin/bc) == "1" ]; then
echo "kernel.shmmax too low, setting to 1GB"
${pkgs.procps}/bin/sysctl -w kernel.shmmax=1073741824
fi
if [ $(echo "$(cat /proc/sys/net/core/rmem_max) < 15728640" | ${pkgs.bc}/bin/bc) == "1" ]; then
echo "increasing socket buffer limit (/proc/sys/net/core/rmem_max): $(cat /proc/sys/net/core/rmem_max) -> 15728640"
echo 15728640 > /proc/sys/net/core/rmem_max
fi
if [ $(echo "$(cat /proc/sys/net/core/wmem_max) < 5242880" | ${pkgs.bc}/bin/bc) == "1" ]; then
echo "increasing socket buffer limit (/proc/sys/net/core/wmem_max): $(cat /proc/sys/net/core/wmem_max) -> 5242880"
echo 5242880 > /proc/sys/net/core/wmem_max
fi
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}"
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/smd"
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/udf"
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/udf/lua"
'';
};
};
}

View file

@ -0,0 +1,563 @@
{ config, lib, pkgs, ... }:
let
inherit (lib)
concatStringsSep
flip
literalDocBook
literalExpression
optionalAttrs
optionals
recursiveUpdate
mkEnableOption
mkIf
mkOption
types
versionAtLeast
;
cfg = config.services.cassandra;
defaultUser = "cassandra";
cassandraConfig = flip recursiveUpdate cfg.extraConfig (
{
commitlog_sync = "batch";
commitlog_sync_batch_window_in_ms = 2;
start_native_transport = cfg.allowClients;
cluster_name = cfg.clusterName;
partitioner = "org.apache.cassandra.dht.Murmur3Partitioner";
endpoint_snitch = "SimpleSnitch";
data_file_directories = [ "${cfg.homeDir}/data" ];
commitlog_directory = "${cfg.homeDir}/commitlog";
saved_caches_directory = "${cfg.homeDir}/saved_caches";
} // optionalAttrs (cfg.seedAddresses != [ ]) {
seed_provider = [
{
class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
parameters = [{ seeds = concatStringsSep "," cfg.seedAddresses; }];
}
];
} // optionalAttrs (versionAtLeast cfg.package.version "3") {
hints_directory = "${cfg.homeDir}/hints";
}
);
cassandraConfigWithAddresses = cassandraConfig // (
if cfg.listenAddress == null
then { listen_interface = cfg.listenInterface; }
else { listen_address = cfg.listenAddress; }
) // (
if cfg.rpcAddress == null
then { rpc_interface = cfg.rpcInterface; }
else { rpc_address = cfg.rpcAddress; }
);
cassandraEtc = pkgs.stdenv.mkDerivation {
name = "cassandra-etc";
cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
passAsFile = [ "extraEnvSh" ];
inherit (cfg) extraEnvSh;
buildCommand = ''
mkdir -p "$out"
echo "$cassandraYaml" > "$out/cassandra.yaml"
ln -s "$cassandraLogbackConfig" "$out/logback.xml"
( cat "$cassandraEnvPkg"
echo "# lines from services.cassandra.extraEnvSh: "
cat "$extraEnvShPath"
) > "$out/cassandra-env.sh"
# Delete default JMX Port, otherwise we can't set it using env variable
sed -i '/JMX_PORT="7199"/d' "$out/cassandra-env.sh"
# Delete default password file
sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh"
'';
};
defaultJmxRolesFile =
builtins.foldl'
(left: right: left + right) ""
(map (role: "${role.username} ${role.password}") cfg.jmxRoles);
fullJvmOptions =
cfg.jvmOpts
++ optionals (cfg.jmxRoles != [ ]) [
"-Dcom.sun.management.jmxremote.authenticate=true"
"-Dcom.sun.management.jmxremote.password.file=${cfg.jmxRolesFile}"
] ++ optionals cfg.remoteJmx [
"-Djava.rmi.server.hostname=${cfg.rpcAddress}"
];
in
{
options.services.cassandra = {
enable = mkEnableOption ''
Apache Cassandra Scalable and highly available database.
'';
clusterName = mkOption {
type = types.str;
default = "Test Cluster";
description = ''
The name of the cluster.
This setting prevents nodes in one logical cluster from joining
another. All nodes in a cluster must have the same value.
'';
};
user = mkOption {
type = types.str;
default = defaultUser;
description = "Run Apache Cassandra under this user.";
};
group = mkOption {
type = types.str;
default = defaultUser;
description = "Run Apache Cassandra under this group.";
};
homeDir = mkOption {
type = types.path;
default = "/var/lib/cassandra";
description = ''
Home directory for Apache Cassandra.
'';
};
package = mkOption {
type = types.package;
default = pkgs.cassandra;
defaultText = literalExpression "pkgs.cassandra";
example = literalExpression "pkgs.cassandra_3_11";
description = ''
The Apache Cassandra package to use.
'';
};
jvmOpts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Populate the JVM_OPT environment variable.
'';
};
listenAddress = mkOption {
type = types.nullOr types.str;
default = "127.0.0.1";
example = null;
description = ''
Address or interface to bind to and tell other Cassandra nodes
to connect to. You _must_ change this if you want multiple
nodes to be able to communicate!
Set listenAddress OR listenInterface, not both.
Leaving it blank leaves it up to
InetAddress.getLocalHost(). This will always do the Right
Thing _if_ the node is properly configured (hostname, name
resolution, etc), and the Right Thing is to use the address
associated with the hostname (it might not be).
Setting listen_address to 0.0.0.0 is always wrong.
'';
};
listenInterface = mkOption {
type = types.nullOr types.str;
default = null;
example = "eth1";
description = ''
Set listenAddress OR listenInterface, not both. Interfaces
must correspond to a single address, IP aliasing is not
supported.
'';
};
rpcAddress = mkOption {
type = types.nullOr types.str;
default = "127.0.0.1";
example = null;
description = ''
The address or interface to bind the native transport server to.
Set rpcAddress OR rpcInterface, not both.
Leaving rpcAddress blank has the same effect as on
listenAddress (i.e. it will be based on the configured hostname
of the node).
Note that unlike listenAddress, you can specify 0.0.0.0, but you
must also set extraConfig.broadcast_rpc_address to a value other
than 0.0.0.0.
For security reasons, you should not expose this port to the
internet. Firewall it if needed.
'';
};
rpcInterface = mkOption {
type = types.nullOr types.str;
default = null;
example = "eth1";
description = ''
Set rpcAddress OR rpcInterface, not both. Interfaces must
correspond to a single address, IP aliasing is not supported.
'';
};
logbackConfig = mkOption {
type = types.lines;
default = ''
<configuration scan="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<logger name="com.thinkaurelius.thrift" level="ERROR"/>
</configuration>
'';
description = ''
XML logback configuration for cassandra
'';
};
seedAddresses = mkOption {
type = types.listOf types.str;
default = [ "127.0.0.1" ];
description = ''
The addresses of hosts designated as contact points in the cluster. A
joining node contacts one of the nodes in the seeds list to learn the
topology of the ring.
Set to 127.0.0.1 for a single node cluster.
'';
};
allowClients = mkOption {
type = types.bool;
default = true;
description = ''
Enables or disables the native transport server (CQL binary protocol).
This server uses the same address as the <literal>rpcAddress</literal>,
but the port it uses is not <literal>rpc_port</literal> but
<literal>native_transport_port</literal>. See the official Cassandra
docs for more information on these variables and set them using
<literal>extraConfig</literal>.
'';
};
extraConfig = mkOption {
type = types.attrs;
default = { };
example =
{
commitlog_sync_batch_window_in_ms = 3;
};
description = ''
Extra options to be merged into cassandra.yaml as nix attribute set.
'';
};
extraEnvSh = mkOption {
type = types.lines;
default = "";
example = literalExpression ''"CLASSPATH=$CLASSPATH:''${extraJar}"'';
description = ''
Extra shell lines to be appended onto cassandra-env.sh.
'';
};
fullRepairInterval = mkOption {
type = types.nullOr types.str;
default = "3w";
example = null;
description = ''
Set the interval how often full repairs are run, i.e.
<literal>nodetool repair --full</literal> is executed. See
https://cassandra.apache.org/doc/latest/operating/repair.html
for more information.
Set to <literal>null</literal> to disable full repairs.
'';
};
fullRepairOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--partitioner-range" ];
description = ''
Options passed through to the full repair command.
'';
};
incrementalRepairInterval = mkOption {
type = types.nullOr types.str;
default = "3d";
example = null;
description = ''
Set the interval how often incremental repairs are run, i.e.
<literal>nodetool repair</literal> is executed. See
https://cassandra.apache.org/doc/latest/operating/repair.html
for more information.
Set to <literal>null</literal> to disable incremental repairs.
'';
};
incrementalRepairOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--partitioner-range" ];
description = ''
Options passed through to the incremental repair command.
'';
};
maxHeapSize = mkOption {
type = types.nullOr types.str;
default = null;
example = "4G";
description = ''
Must be left blank or set together with heapNewSize.
If left blank a sensible value for the available amount of RAM and CPU
cores is calculated.
Override to set the amount of memory to allocate to the JVM at
start-up. For production use you may wish to adjust this for your
environment. MAX_HEAP_SIZE is the total amount of memory dedicated
to the Java heap. HEAP_NEWSIZE refers to the size of the young
generation.
The main trade-off for the young generation is that the larger it
is, the longer GC pause times will be. The shorter it is, the more
expensive GC will be (usually).
'';
};
heapNewSize = mkOption {
type = types.nullOr types.str;
default = null;
example = "800M";
description = ''
Must be left blank or set together with heapNewSize.
If left blank a sensible value for the available amount of RAM and CPU
cores is calculated.
Override to set the amount of memory to allocate to the JVM at
start-up. For production use you may wish to adjust this for your
environment. HEAP_NEWSIZE refers to the size of the young
generation.
The main trade-off for the young generation is that the larger it
is, the longer GC pause times will be. The shorter it is, the more
expensive GC will be (usually).
The example HEAP_NEWSIZE assumes a modern 8-core+ machine for decent pause
times. If in doubt, and if you do not particularly want to tweak, go with
100 MB per physical CPU core.
'';
};
mallocArenaMax = mkOption {
type = types.nullOr types.int;
default = null;
example = 4;
description = ''
Set this to control the amount of arenas per-thread in glibc.
'';
};
remoteJmx = mkOption {
type = types.bool;
default = false;
description = ''
Cassandra ships with JMX accessible *only* from localhost.
To enable remote JMX connections set to true.
Be sure to also enable authentication and/or TLS.
See: https://wiki.apache.org/cassandra/JmxSecurity
'';
};
jmxPort = mkOption {
type = types.int;
default = 7199;
description = ''
Specifies the default port over which Cassandra will be available for
JMX connections.
For security reasons, you should not expose this port to the internet.
Firewall it if needed.
'';
};
jmxRoles = mkOption {
default = [ ];
description = ''
Roles that are allowed to access the JMX (e.g. nodetool)
BEWARE: The passwords will be stored world readable in the nix-store.
It's recommended to use your own protected file using
<literal>jmxRolesFile</literal>
Doesn't work in versions older than 3.11 because they don't like that
it's world readable.
'';
type = types.listOf (types.submodule {
options = {
username = mkOption {
type = types.str;
description = "Username for JMX";
};
password = mkOption {
type = types.str;
description = "Password for JMX";
};
};
});
};
jmxRolesFile = mkOption {
type = types.nullOr types.path;
default =
if versionAtLeast cfg.package.version "3.11"
then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
else null;
defaultText = literalDocBook ''generated configuration file if version is at least 3.11, otherwise <literal>null</literal>'';
example = "/var/lib/cassandra/jmx.password";
description = ''
Specify your own jmx roles file.
Make sure the permissions forbid "others" from reading the file if
you're using Cassandra below version 3.11.
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (cfg.listenAddress == null) != (cfg.listenInterface == null);
message = "You have to set either listenAddress or listenInterface";
}
{
assertion = (cfg.rpcAddress == null) != (cfg.rpcInterface == null);
message = "You have to set either rpcAddress or rpcInterface";
}
{
assertion = (cfg.maxHeapSize == null) == (cfg.heapNewSize == null);
message = "If you set either of maxHeapSize or heapNewSize you have to set both";
}
{
assertion = cfg.remoteJmx -> cfg.jmxRolesFile != null;
message = ''
If you want JMX available remotely you need to set a password using
<literal>jmxRoles</literal> or <literal>jmxRolesFile</literal> if
using Cassandra older than v3.11.
'';
}
];
users = mkIf (cfg.user == defaultUser) {
users.${defaultUser} = {
group = cfg.group;
home = cfg.homeDir;
createHome = true;
uid = config.ids.uids.cassandra;
description = "Cassandra service user";
};
groups.${defaultUser}.gid = config.ids.gids.cassandra;
};
systemd.services.cassandra = {
description = "Apache Cassandra service";
after = [ "network.target" ];
environment = {
CASSANDRA_CONF = "${cassandraEtc}";
JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions;
MAX_HEAP_SIZE = toString cfg.maxHeapSize;
HEAP_NEWSIZE = toString cfg.heapNewSize;
MALLOC_ARENA_MAX = toString cfg.mallocArenaMax;
LOCAL_JMX = if cfg.remoteJmx then "no" else "yes";
JMX_PORT = toString cfg.jmxPort;
};
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/cassandra -f";
SuccessExitStatus = 143;
};
};
systemd.services.cassandra-full-repair = {
description = "Perform a full repair on this Cassandra node";
after = [ "cassandra.service" ];
requires = [ "cassandra.service" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart =
concatStringsSep " "
([
"${cfg.package}/bin/nodetool"
"repair"
"--full"
] ++ cfg.fullRepairOptions);
};
};
systemd.timers.cassandra-full-repair =
mkIf (cfg.fullRepairInterval != null) {
description = "Schedule full repairs on Cassandra";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = cfg.fullRepairInterval;
OnUnitActiveSec = cfg.fullRepairInterval;
Persistent = true;
};
};
systemd.services.cassandra-incremental-repair = {
description = "Perform an incremental repair on this cassandra node.";
after = [ "cassandra.service" ];
requires = [ "cassandra.service" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart =
concatStringsSep " "
([
"${cfg.package}/bin/nodetool"
"repair"
] ++ cfg.incrementalRepairOptions);
};
};
systemd.timers.cassandra-incremental-repair =
mkIf (cfg.incrementalRepairInterval != null) {
description = "Schedule incremental repairs on Cassandra";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = cfg.incrementalRepairInterval;
OnUnitActiveSec = cfg.incrementalRepairInterval;
Persistent = true;
};
};
};
meta.maintainers = with lib.maintainers; [ roberth ];
}

View file

@ -0,0 +1,78 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.clickhouse;
in
with lib;
{
###### interface
options = {
services.clickhouse = {
enable = mkEnableOption "ClickHouse database server";
package = mkOption {
type = types.package;
default = pkgs.clickhouse;
defaultText = "pkgs.clickhouse";
description = ''
ClickHouse package to use.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
users.users.clickhouse = {
name = "clickhouse";
uid = config.ids.uids.clickhouse;
group = "clickhouse";
description = "ClickHouse server user";
};
users.groups.clickhouse.gid = config.ids.gids.clickhouse;
systemd.services.clickhouse = {
description = "ClickHouse server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = "clickhouse";
Group = "clickhouse";
ConfigurationDirectory = "clickhouse-server";
AmbientCapabilities = "CAP_SYS_NICE";
StateDirectory = "clickhouse";
LogsDirectory = "clickhouse";
ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=${cfg.package}/etc/clickhouse-server/config.xml";
};
};
environment.etc = {
"clickhouse-server/config.xml" = {
source = "${cfg.package}/etc/clickhouse-server/config.xml";
};
"clickhouse-server/users.xml" = {
source = "${cfg.package}/etc/clickhouse-server/users.xml";
};
};
environment.systemPackages = [ cfg.package ];
# startup requires a `/etc/localtime` which only if exists if `time.timeZone != null`
time.timeZone = mkDefault "UTC";
};
}

View file

@ -0,0 +1,225 @@
{ config, lib, pkgs, utils, ... }:
with lib;
let
cfg = config.services.cockroachdb;
crdb = cfg.package;
startupCommand = utils.escapeSystemdExecArgs
([
# Basic startup
"${crdb}/bin/cockroach"
"start"
"--logtostderr"
"--store=/var/lib/cockroachdb"
# WebUI settings
"--http-addr=${cfg.http.address}:${toString cfg.http.port}"
# Cluster listen address
"--listen-addr=${cfg.listen.address}:${toString cfg.listen.port}"
# Cache and memory settings.
"--cache=${cfg.cache}"
"--max-sql-memory=${cfg.maxSqlMemory}"
# Certificate/security settings.
(if cfg.insecure then "--insecure" else "--certs-dir=${cfg.certsDir}")
]
++ lib.optional (cfg.join != null) "--join=${cfg.join}"
++ lib.optional (cfg.locality != null) "--locality=${cfg.locality}"
++ cfg.extraArgs);
addressOption = descr: defaultPort: {
address = mkOption {
type = types.str;
default = "localhost";
description = "Address to bind to for ${descr}";
};
port = mkOption {
type = types.port;
default = defaultPort;
description = "Port to bind to for ${descr}";
};
};
in
{
options = {
services.cockroachdb = {
enable = mkEnableOption "CockroachDB Server";
listen = addressOption "intra-cluster communication" 26257;
http = addressOption "http-based Admin UI" 8080;
locality = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
An ordered, comma-separated list of key-value pairs that describe the
topography of the machine. Topography might include country,
datacenter or rack designations. Data is automatically replicated to
maximize diversities of each tier. The order of tiers is used to
determine the priority of the diversity, so the more inclusive
localities like country should come before less inclusive localities
like datacenter. The tiers and order must be the same on all nodes.
Including more tiers is better than including fewer. For example:
<literal>
country=us,region=us-west,datacenter=us-west-1b,rack=12
country=ca,region=ca-east,datacenter=ca-east-2,rack=4
planet=earth,province=manitoba,colo=secondary,power=3
</literal>
'';
};
join = mkOption {
type = types.nullOr types.str;
default = null;
description = "The addresses for connecting the node to a cluster.";
};
insecure = mkOption {
type = types.bool;
default = false;
description = "Run in insecure mode.";
};
certsDir = mkOption {
type = types.nullOr types.path;
default = null;
description = "The path to the certificate directory.";
};
user = mkOption {
type = types.str;
default = "cockroachdb";
description = "User account under which CockroachDB runs";
};
group = mkOption {
type = types.str;
default = "cockroachdb";
description = "User account under which CockroachDB runs";
};
openPorts = mkOption {
type = types.bool;
default = false;
description = "Open firewall ports for cluster communication by default";
};
cache = mkOption {
type = types.str;
default = "25%";
description = ''
The total size for caches.
This can be a percentage, expressed with a fraction sign or as a
decimal-point number, or any bytes-based unit. For example,
<literal>"25%"</literal>, <literal>"0.25"</literal> both represent
25% of the available system memory. The values
<literal>"1000000000"</literal> and <literal>"1GB"</literal> both
represent 1 gigabyte of memory.
'';
};
maxSqlMemory = mkOption {
type = types.str;
default = "25%";
description = ''
The maximum in-memory storage capacity available to store temporary
data for SQL queries.
This can be a percentage, expressed with a fraction sign or as a
decimal-point number, or any bytes-based unit. For example,
<literal>"25%"</literal>, <literal>"0.25"</literal> both represent
25% of the available system memory. The values
<literal>"1000000000"</literal> and <literal>"1GB"</literal> both
represent 1 gigabyte of memory.
'';
};
package = mkOption {
type = types.package;
default = pkgs.cockroachdb;
defaultText = literalExpression "pkgs.cockroachdb";
description = ''
The CockroachDB derivation to use for running the service.
This would primarily be useful to enable Enterprise Edition features
in your own custom CockroachDB build (Nixpkgs CockroachDB binaries
only contain open source features and open source code).
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [];
example = [ "--advertise-addr" "[fe80::f6f2:::]" ];
description = ''
Extra CLI arguments passed to <command>cockroach start</command>.
For the full list of supported argumemnts, check <link xlink:href="https://www.cockroachlabs.com/docs/stable/cockroach-start.html#flags"/>
'';
};
};
};
config = mkIf config.services.cockroachdb.enable {
assertions = [
{ assertion = !cfg.insecure -> cfg.certsDir != null;
message = "CockroachDB must have a set of SSL certificates (.certsDir), or run in Insecure Mode (.insecure = true)";
}
];
environment.systemPackages = [ crdb ];
users.users = optionalAttrs (cfg.user == "cockroachdb") {
cockroachdb = {
description = "CockroachDB Server User";
uid = config.ids.uids.cockroachdb;
group = cfg.group;
};
};
users.groups = optionalAttrs (cfg.group == "cockroachdb") {
cockroachdb.gid = config.ids.gids.cockroachdb;
};
networking.firewall.allowedTCPPorts = lib.optionals cfg.openPorts
[ cfg.http.port cfg.listen.port ];
systemd.services.cockroachdb =
{ description = "CockroachDB Server";
documentation = [ "man:cockroach(1)" "https://www.cockroachlabs.com" ];
after = [ "network.target" "time-sync.target" ];
requires = [ "time-sync.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = "/var/lib/cockroachdb";
serviceConfig =
{ ExecStart = startupCommand;
Type = "notify";
User = cfg.user;
StateDirectory = "cockroachdb";
StateDirectoryMode = "0700";
Restart = "always";
# A conservative-ish timeout is alright here, because for Type=notify
# cockroach will send systemd pings during startup to keep it alive
TimeoutStopSec = 60;
RestartSec = 10;
};
};
};
meta.maintainers = with lib.maintainers; [ thoughtpolice ];
}

View file

@ -0,0 +1,231 @@
{ config, options, lib, pkgs, ... }:
with lib;
let
cfg = config.services.couchdb;
opt = options.services.couchdb;
configFile = pkgs.writeText "couchdb.ini" (
''
[couchdb]
database_dir = ${cfg.databaseDir}
uri_file = ${cfg.uriFile}
view_index_dir = ${cfg.viewIndexDir}
'' + (optionalString (cfg.adminPass != null) ''
[admins]
${cfg.adminUser} = ${cfg.adminPass}
'' + ''
[chttpd]
'') +
''
port = ${toString cfg.port}
bind_address = ${cfg.bindAddress}
[log]
file = ${cfg.logFile}
'');
executable = "${cfg.package}/bin/couchdb";
in {
###### interface
options = {
services.couchdb = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to run CouchDB Server.
'';
};
package = mkOption {
type = types.package;
default = pkgs.couchdb3;
defaultText = literalExpression "pkgs.couchdb3";
description = ''
CouchDB package to use.
'';
};
adminUser = mkOption {
type = types.str;
default = "admin";
description = ''
Couchdb (i.e. fauxton) account with permission for all dbs and
tasks.
'';
};
adminPass = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Couchdb (i.e. fauxton) account with permission for all dbs and
tasks.
'';
};
user = mkOption {
type = types.str;
default = "couchdb";
description = ''
User account under which couchdb runs.
'';
};
group = mkOption {
type = types.str;
default = "couchdb";
description = ''
Group account under which couchdb runs.
'';
};
# couchdb options: http://docs.couchdb.org/en/latest/config/index.html
databaseDir = mkOption {
type = types.path;
default = "/var/lib/couchdb";
description = ''
Specifies location of CouchDB database files (*.couch named). This
location should be writable and readable for the user the CouchDB
service runs as (couchdb by default).
'';
};
uriFile = mkOption {
type = types.path;
default = "/run/couchdb/couchdb.uri";
description = ''
This file contains the full URI that can be used to access this
instance of CouchDB. It is used to help discover the port CouchDB is
running on (if it was set to 0 (e.g. automatically assigned any free
one). This file should be writable and readable for the user that
runs the CouchDB service (couchdb by default).
'';
};
viewIndexDir = mkOption {
type = types.path;
default = "/var/lib/couchdb";
description = ''
Specifies location of CouchDB view index files. This location should
be writable and readable for the user that runs the CouchDB service
(couchdb by default).
'';
};
bindAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Defines the IP address by which CouchDB will be accessible.
'';
};
port = mkOption {
type = types.int;
default = 5984;
description = ''
Defined the port number to listen.
'';
};
logFile = mkOption {
type = types.path;
default = "/var/log/couchdb.log";
description = ''
Specifies the location of file for logging output.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration. Overrides any other cofiguration.
'';
};
argsFile = mkOption {
type = types.path;
default = "${cfg.package}/etc/vm.args";
defaultText = literalExpression ''"config.${opt.package}/etc/vm.args"'';
description = ''
vm.args configuration. Overrides Couchdb's Erlang VM parameters file.
'';
};
configFile = mkOption {
type = types.path;
description = ''
Configuration file for persisting runtime changes. File
needs to be readable and writable from couchdb user/group.
'';
};
};
};
###### implementation
config = mkIf config.services.couchdb.enable {
environment.systemPackages = [ cfg.package ];
services.couchdb.configFile = mkDefault "/var/lib/couchdb/local.ini";
systemd.tmpfiles.rules = [
"d '${dirOf cfg.uriFile}' - ${cfg.user} ${cfg.group} - -"
"f '${cfg.logFile}' - ${cfg.user} ${cfg.group} - -"
"d '${cfg.databaseDir}' - ${cfg.user} ${cfg.group} - -"
"d '${cfg.viewIndexDir}' - ${cfg.user} ${cfg.group} - -"
];
systemd.services.couchdb = {
description = "CouchDB Server";
wantedBy = [ "multi-user.target" ];
preStart = ''
touch ${cfg.configFile}
if ! test -e ${cfg.databaseDir}/.erlang.cookie; then
touch ${cfg.databaseDir}/.erlang.cookie
chmod 600 ${cfg.databaseDir}/.erlang.cookie
dd if=/dev/random bs=16 count=1 | base64 > ${cfg.databaseDir}/.erlang.cookie
fi
'';
environment = {
# we are actually specifying 5 configuration files:
# 1. the preinstalled default.ini
# 2. the module configuration
# 3. the extraConfig from the module options
# 4. the locally writable config file, which couchdb itself writes to
ERL_FLAGS= ''-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}'';
# 5. the vm.args file
COUCHDB_ARGS_FILE=''${cfg.argsFile}'';
HOME =''${cfg.databaseDir}'';
};
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = executable;
};
};
users.users.couchdb = {
description = "CouchDB Server user";
group = "couchdb";
uid = config.ids.uids.couchdb;
};
users.groups.couchdb.gid = config.ids.gids.couchdb;
};
}

View file

@ -0,0 +1,152 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.dragonflydb;
dragonflydb = pkgs.dragonflydb;
settings =
{
port = cfg.port;
dir = "/var/lib/dragonflydb";
keys_output_limit = cfg.keysOutputLimit;
} //
(lib.optionalAttrs (cfg.bind != null) { bind = cfg.bind; }) //
(lib.optionalAttrs (cfg.requirePass != null) { requirepass = cfg.requirePass; }) //
(lib.optionalAttrs (cfg.maxMemory != null) { maxmemory = cfg.maxMemory; }) //
(lib.optionalAttrs (cfg.memcachePort != null) { memcache_port = cfg.memcachePort; }) //
(lib.optionalAttrs (cfg.dbNum != null) { dbnum = cfg.dbNum; }) //
(lib.optionalAttrs (cfg.cacheMode != null) { cache_mode = cfg.cacheMode; });
in
{
###### interface
options = {
services.dragonflydb = {
enable = mkEnableOption "DragonflyDB";
user = mkOption {
type = types.str;
default = "dragonfly";
description = "The user to run DragonflyDB as";
};
port = mkOption {
type = types.port;
default = 6379;
description = "The TCP port to accept connections.";
};
bind = mkOption {
type = with types; nullOr str;
default = "127.0.0.1";
description = ''
The IP interface to bind to.
<literal>null</literal> means "all interfaces".
'';
};
requirePass = mkOption {
type = with types; nullOr str;
default = null;
description = "Password for database";
example = "letmein!";
};
maxMemory = mkOption {
type = with types; nullOr ints.unsigned;
default = null;
description = ''
The maximum amount of memory to use for storage (in bytes).
<literal>null</literal> means this will be automatically set.
'';
};
memcachePort = mkOption {
type = with types; nullOr port;
default = null;
description = ''
To enable memcached compatible API on this port.
<literal>null</literal> means disabled.
'';
};
keysOutputLimit = mkOption {
type = types.ints.unsigned;
default = 8192;
description = ''
Maximum number of returned keys in keys command.
<literal>keys</literal> is a dangerous command.
We truncate its result to avoid blowup in memory when fetching too many keys.
'';
};
dbNum = mkOption {
type = with types; nullOr ints.unsigned;
default = null;
description = "Maximum number of supported databases for <literal>select</literal>";
};
cacheMode = mkOption {
type = with types; nullOr bool;
default = null;
description = ''
Once this mode is on, Dragonfly will evict items least likely to be stumbled
upon in the future but only when it is near maxmemory limit.
'';
};
};
};
###### implementation
config = mkIf config.services.dragonflydb.enable {
users.users = optionalAttrs (cfg.user == "dragonfly") {
dragonfly.description = "DragonflyDB server user";
dragonfly.isSystemUser = true;
dragonfly.group = "dragonfly";
};
users.groups = optionalAttrs (cfg.user == "dragonfly") { dragonfly = { }; };
environment.systemPackages = [ dragonflydb ];
systemd.services.dragonflydb = {
description = "DragonflyDB server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${dragonflydb}/bin/dragonfly --alsologtostderr ${builtins.concatStringsSep " " (attrsets.mapAttrsToList (n: v: "--${n} ${strings.escapeShellArg v}") settings)}";
User = cfg.user;
# Filesystem access
ReadWritePaths = [ settings.dir ];
StateDirectory = "dragonflydb";
StateDirectoryMode = "0700";
# Process Properties
LimitMEMLOCK = "infinity";
# Caps
CapabilityBoundingSet = "";
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
LockPersonality = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictRealtime = true;
PrivateMounts = true;
MemoryDenyWriteExecute = true;
};
};
};
}

View file

@ -0,0 +1,168 @@
{ config, lib, pkgs, ... }:
# TODO: This may file may need additional review, eg which configuartions to
# expose to the user.
#
# I only used it to access some simple databases.
# test:
# isql, then type the following commands:
# CREATE DATABASE '/var/db/firebird/data/test.fdb' USER 'SYSDBA' PASSWORD 'masterkey';
# CONNECT '/var/db/firebird/data/test.fdb' USER 'SYSDBA' PASSWORD 'masterkey';
# CREATE TABLE test ( text varchar(100) );
# DROP DATABASE;
#
# Be careful, virtuoso-opensource also provides a different isql command !
# There are at least two ways to run firebird. superserver has been choosen
# however there are no strong reasons to prefer this or the other one AFAIK
# Eg superserver is said to be most efficiently using resources according to
# http://www.firebirdsql.org/manual/qsg25-classic-or-super.html
with lib;
let
cfg = config.services.firebird;
firebird = cfg.package;
dataDir = "${cfg.baseDir}/data";
systemDir = "${cfg.baseDir}/system";
in
{
###### interface
options = {
services.firebird = {
enable = mkEnableOption "the Firebird super server";
package = mkOption {
default = pkgs.firebird;
defaultText = literalExpression "pkgs.firebird";
type = types.package;
example = literalExpression "pkgs.firebird_3";
description = ''
Which Firebird package to be installed: <code>pkgs.firebird_3</code>
For SuperServer use override: <code>pkgs.firebird_3.override { superServer = true; };</code>
'';
};
port = mkOption {
default = 3050;
type = types.port;
description = ''
Port Firebird uses.
'';
};
user = mkOption {
default = "firebird";
type = types.str;
description = ''
User account under which firebird runs.
'';
};
baseDir = mkOption {
default = "/var/lib/firebird";
type = types.str;
description = ''
Location containing data/ and system/ directories.
data/ stores the databases, system/ stores the password database security2.fdb.
'';
};
};
};
###### implementation
config = mkIf config.services.firebird.enable {
environment.systemPackages = [cfg.package];
systemd.tmpfiles.rules = [
"d '${dataDir}' 0700 ${cfg.user} - - -"
"d '${systemDir}' 0700 ${cfg.user} - - -"
];
systemd.services.firebird =
{ description = "Firebird Super-Server";
wantedBy = [ "multi-user.target" ];
# TODO: moving security2.fdb into the data directory works, maybe there
# is a better way
preStart =
''
if ! test -e "${systemDir}/security2.fdb"; then
cp ${firebird}/security2.fdb "${systemDir}"
fi
if ! test -e "${systemDir}/security3.fdb"; then
cp ${firebird}/security3.fdb "${systemDir}"
fi
if ! test -e "${systemDir}/security4.fdb"; then
cp ${firebird}/security4.fdb "${systemDir}"
fi
chmod -R 700 "${dataDir}" "${systemDir}" /var/log/firebird
'';
serviceConfig.User = cfg.user;
serviceConfig.LogsDirectory = "firebird";
serviceConfig.LogsDirectoryMode = "0700";
serviceConfig.ExecStart = "${firebird}/bin/fbserver -d";
# TODO think about shutdown
};
environment.etc."firebird/firebird.msg".source = "${firebird}/firebird.msg";
# think about this again - and eventually make it an option
environment.etc."firebird/firebird.conf".text = ''
# RootDirectory = Restrict ${dataDir}
DatabaseAccess = Restrict ${dataDir}
ExternalFileAccess = Restrict ${dataDir}
# what is this? is None allowed?
UdfAccess = None
# "Native" = traditional interbase/firebird, "mixed" is windows only
Authentication = Native
# defaults to -1 on non Win32
#MaxUnflushedWrites = 100
#MaxUnflushedWriteTime = 100
# show trace if trouble occurs (does this require debug build?)
# BugcheckAbort = 0
# ConnectionTimeout = 180
#RemoteServiceName = gds_db
RemoteServicePort = ${cfg.port}
# randomly choose port for server Event Notification
#RemoteAuxPort = 0
# rsetrict connections to a network card:
#RemoteBindAddress =
# there are some additional settings which should be reviewed
'';
users.users.firebird = {
description = "Firebird server user";
group = "firebird";
uid = config.ids.uids.firebird;
};
users.groups.firebird.gid = config.ids.gids.firebird;
};
}

View file

@ -0,0 +1,429 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.foundationdb;
pkg = cfg.package;
# used for initial cluster configuration
initialIpAddr = if (cfg.publicAddress != "auto") then cfg.publicAddress else "127.0.0.1";
fdbServers = n:
concatStringsSep "\n" (map (x: "[fdbserver.${toString (x+cfg.listenPortStart)}]") (range 0 (n - 1)));
backupAgents = n:
concatStringsSep "\n" (map (x: "[backup_agent.${toString x}]") (range 1 n));
configFile = pkgs.writeText "foundationdb.conf" ''
[general]
cluster_file = /etc/foundationdb/fdb.cluster
[fdbmonitor]
restart_delay = ${toString cfg.restartDelay}
user = ${cfg.user}
group = ${cfg.group}
[fdbserver]
command = ${pkg}/bin/fdbserver
public_address = ${cfg.publicAddress}:$ID
listen_address = ${cfg.listenAddress}
datadir = ${cfg.dataDir}/$ID
logdir = ${cfg.logDir}
logsize = ${cfg.logSize}
maxlogssize = ${cfg.maxLogSize}
${optionalString (cfg.class != null) "class = ${cfg.class}"}
memory = ${cfg.memory}
storage_memory = ${cfg.storageMemory}
${optionalString (lib.versionAtLeast cfg.package.version "6.1") ''
trace_format = ${cfg.traceFormat}
''}
${optionalString (cfg.tls != null) ''
tls_plugin = ${pkg}/libexec/plugins/FDBLibTLS.so
tls_certificate_file = ${cfg.tls.certificate}
tls_key_file = ${cfg.tls.key}
tls_verify_peers = ${cfg.tls.allowedPeers}
''}
${optionalString (cfg.locality.machineId != null) "locality_machineid=${cfg.locality.machineId}"}
${optionalString (cfg.locality.zoneId != null) "locality_zoneid=${cfg.locality.zoneId}"}
${optionalString (cfg.locality.datacenterId != null) "locality_dcid=${cfg.locality.datacenterId}"}
${optionalString (cfg.locality.dataHall != null) "locality_data_hall=${cfg.locality.dataHall}"}
${fdbServers cfg.serverProcesses}
[backup_agent]
command = ${pkg}/libexec/backup_agent
${backupAgents cfg.backupProcesses}
'';
in
{
options.services.foundationdb = {
enable = mkEnableOption "FoundationDB Server";
package = mkOption {
type = types.package;
description = ''
The FoundationDB package to use for this server. This must be specified by the user
in order to ensure migrations and upgrades are controlled appropriately.
'';
};
publicAddress = mkOption {
type = types.str;
default = "auto";
description = "Publicly visible IP address of the process. Port is determined by process ID";
};
listenAddress = mkOption {
type = types.str;
default = "public";
description = "Publicly visible IP address of the process. Port is determined by process ID";
};
listenPortStart = mkOption {
type = types.int;
default = 4500;
description = ''
Starting port number for database listening sockets. Every FDB process binds to a
subsequent port, to this number reflects the start of the overall range. e.g. having
8 server processes will use all ports between 4500 and 4507.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open the firewall ports corresponding to FoundationDB processes and coordinators
using <option>config.networking.firewall.*</option>.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/foundationdb";
description = "Data directory. All cluster data will be put under here.";
};
logDir = mkOption {
type = types.path;
default = "/var/log/foundationdb";
description = "Log directory.";
};
user = mkOption {
type = types.str;
default = "foundationdb";
description = "User account under which FoundationDB runs.";
};
group = mkOption {
type = types.str;
default = "foundationdb";
description = "Group account under which FoundationDB runs.";
};
class = mkOption {
type = types.nullOr (types.enum [ "storage" "transaction" "stateless" ]);
default = null;
description = "Process class";
};
restartDelay = mkOption {
type = types.int;
default = 10;
description = "Number of seconds to wait before restarting servers.";
};
logSize = mkOption {
type = types.str;
default = "10MiB";
description = ''
Roll over to a new log file after the current log file
reaches the specified size.
'';
};
maxLogSize = mkOption {
type = types.str;
default = "100MiB";
description = ''
Delete the oldest log file when the total size of all log
files exceeds the specified size. If set to 0, old log files
will not be deleted.
'';
};
serverProcesses = mkOption {
type = types.int;
default = 1;
description = "Number of fdbserver processes to run.";
};
backupProcesses = mkOption {
type = types.int;
default = 1;
description = "Number of backup_agent processes to run for snapshots.";
};
memory = mkOption {
type = types.str;
default = "8GiB";
description = ''
Maximum memory used by the process. The default value is
<literal>8GiB</literal>. When specified without a unit,
<literal>MiB</literal> is assumed. This parameter does not
change the memory allocation of the program. Rather, it sets
a hard limit beyond which the process will kill itself and
be restarted. The default value of <literal>8GiB</literal>
is double the intended memory usage in the default
configuration (providing an emergency buffer to deal with
memory leaks or similar problems). It is not recommended to
decrease the value of this parameter below its default
value. It may be increased if you wish to allocate a very
large amount of storage engine memory or cache. In
particular, when the <literal>storageMemory</literal>
parameter is increased, the <literal>memory</literal>
parameter should be increased by an equal amount.
'';
};
storageMemory = mkOption {
type = types.str;
default = "1GiB";
description = ''
Maximum memory used for data storage. The default value is
<literal>1GiB</literal>. When specified without a unit,
<literal>MB</literal> is assumed. Clusters using the memory
storage engine will be restricted to using this amount of
memory per process for purposes of data storage. Memory
overhead associated with storing the data is counted against
this total. If you increase the
<literal>storageMemory</literal>, you should also increase
the <literal>memory</literal> parameter by the same amount.
'';
};
tls = mkOption {
default = null;
description = ''
FoundationDB Transport Security Layer (TLS) settings.
'';
type = types.nullOr (types.submodule ({
options = {
certificate = mkOption {
type = types.str;
description = ''
Path to the TLS certificate file. This certificate will
be offered to, and may be verified by, clients.
'';
};
key = mkOption {
type = types.str;
description = "Private key file for the certificate.";
};
allowedPeers = mkOption {
type = types.str;
default = "Check.Valid=1,Check.Unexpired=1";
description = ''
"Peer verification string". This may be used to adjust which TLS
client certificates a server will accept, as a form of user
authorization; for example, it may only accept TLS clients who
offer a certificate abiding by some locality or organization name.
For more information, please see the FoundationDB documentation.
'';
};
};
}));
};
locality = mkOption {
default = {
machineId = null;
zoneId = null;
datacenterId = null;
dataHall = null;
};
description = ''
FoundationDB locality settings.
'';
type = types.submodule ({
options = {
machineId = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Machine identifier key. All processes on a machine should share a
unique id. By default, processes on a machine determine a unique id to share.
This does not generally need to be set.
'';
};
zoneId = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Zone identifier key. Processes that share a zone id are
considered non-unique for the purposes of data replication.
If unset, defaults to machine id.
'';
};
datacenterId = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Data center identifier key. All processes physically located in a
data center should share the id. If you are depending on data
center based replication this must be set on all processes.
'';
};
dataHall = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Data hall identifier key. All processes physically located in a
data hall should share the id. If you are depending on data
hall based replication this must be set on all processes.
'';
};
};
});
};
extraReadWritePaths = mkOption {
default = [ ];
type = types.listOf types.path;
description = ''
An extra set of filesystem paths that FoundationDB can read to
and write from. By default, FoundationDB runs under a heavily
namespaced systemd environment without write access to most of
the filesystem outside of its data and log directories. By
adding paths to this list, the set of writeable paths will be
expanded. This is useful for allowing e.g. backups to local files,
which must be performed on behalf of the foundationdb service.
'';
};
pidfile = mkOption {
type = types.path;
default = "/run/foundationdb.pid";
description = "Path to pidfile for fdbmonitor.";
};
traceFormat = mkOption {
type = types.enum [ "xml" "json" ];
default = "xml";
description = "Trace logging format.";
};
};
config = mkIf cfg.enable {
assertions = [
{ assertion = lib.versionOlder cfg.package.version "6.1" -> cfg.traceFormat == "xml";
message = ''
Versions of FoundationDB before 6.1 do not support configurable trace formats (only XML is supported).
This option has no effect for version '' + cfg.package.version + '', and enabling it is an error.
'';
}
];
environment.systemPackages = [ pkg ];
users.users = optionalAttrs (cfg.user == "foundationdb") {
foundationdb = {
description = "FoundationDB User";
uid = config.ids.uids.foundationdb;
group = cfg.group;
};
};
users.groups = optionalAttrs (cfg.group == "foundationdb") {
foundationdb.gid = config.ids.gids.foundationdb;
};
networking.firewall.allowedTCPPortRanges = mkIf cfg.openFirewall
[ { from = cfg.listenPortStart;
to = (cfg.listenPortStart + cfg.serverProcesses) - 1;
}
];
systemd.tmpfiles.rules = [
"d /etc/foundationdb 0755 ${cfg.user} ${cfg.group} - -"
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
"d '${cfg.logDir}' 0770 ${cfg.user} ${cfg.group} - -"
"F '${cfg.pidfile}' - ${cfg.user} ${cfg.group} - -"
];
systemd.services.foundationdb = {
description = "FoundationDB Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig =
{ RequiresMountsFor = "${cfg.dataDir} ${cfg.logDir}";
};
serviceConfig =
let rwpaths = [ cfg.dataDir cfg.logDir cfg.pidfile "/etc/foundationdb" ]
++ cfg.extraReadWritePaths;
in
{ Type = "simple";
Restart = "always";
RestartSec = 5;
User = cfg.user;
Group = cfg.group;
PIDFile = "${cfg.pidfile}";
PermissionsStartOnly = true; # setup needs root perms
TimeoutSec = 120; # give reasonable time to shut down
# Security options
NoNewPrivileges = true;
ProtectHome = true;
ProtectSystem = "strict";
ProtectKernelTunables = true;
ProtectControlGroups = true;
PrivateTmp = true;
PrivateDevices = true;
ReadWritePaths = lib.concatStringsSep " " (map (x: "-" + x) rwpaths);
};
path = [ pkg pkgs.coreutils ];
preStart = ''
if [ ! -f /etc/foundationdb/fdb.cluster ]; then
cf=/etc/foundationdb/fdb.cluster
desc=$(tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c8)
rand=$(tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c8)
echo ''${desc}:''${rand}@${initialIpAddr}:${builtins.toString cfg.listenPortStart} > $cf
chmod 0664 $cf
touch "${cfg.dataDir}/.first_startup"
fi
'';
script = "exec fdbmonitor --lockfile ${cfg.pidfile} --conffile ${configFile}";
postStart = ''
if [ -e "${cfg.dataDir}/.first_startup" ]; then
fdbcli --exec "configure new single ssd"
rm -f "${cfg.dataDir}/.first_startup";
fi
'';
};
};
meta.doc = ./foundationdb.xml;
meta.maintainers = with lib.maintainers; [ thoughtpolice ];
}

View file

@ -0,0 +1,443 @@
<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-foundationdb">
<title>FoundationDB</title>
<para>
<emphasis>Source:</emphasis>
<filename>modules/services/databases/foundationdb.nix</filename>
</para>
<para>
<emphasis>Upstream documentation:</emphasis>
<link xlink:href="https://apple.github.io/foundationdb/"/>
</para>
<para>
<emphasis>Maintainer:</emphasis> Austin Seipp
</para>
<para>
<emphasis>Available version(s):</emphasis> 5.1.x, 5.2.x, 6.0.x
</para>
<para>
FoundationDB (or "FDB") is an open source, distributed, transactional
key-value store.
</para>
<section xml:id="module-services-foundationdb-configuring">
<title>Configuring and basic setup</title>
<para>
To enable FoundationDB, add the following to your
<filename>configuration.nix</filename>:
<programlisting>
services.foundationdb.enable = true;
services.foundationdb.package = pkgs.foundationdb52; # FoundationDB 5.2.x
</programlisting>
</para>
<para>
The <option>services.foundationdb.package</option> option is required, and
must always be specified. Due to the fact FoundationDB network protocols and
on-disk storage formats may change between (major) versions, and upgrades
must be explicitly handled by the user, you must always manually specify
this yourself so that the NixOS module will use the proper version. Note
that minor, bugfix releases are always compatible.
</para>
<para>
After running <command>nixos-rebuild</command>, you can verify whether
FoundationDB is running by executing <command>fdbcli</command> (which is
added to <option>environment.systemPackages</option>):
<screen>
<prompt>$ </prompt>sudo -u foundationdb fdbcli
Using cluster file `/etc/foundationdb/fdb.cluster'.
The database is available.
Welcome to the fdbcli. For help, type `help'.
<prompt>fdb> </prompt>status
Using cluster file `/etc/foundationdb/fdb.cluster'.
Configuration:
Redundancy mode - single
Storage engine - memory
Coordinators - 1
Cluster:
FoundationDB processes - 1
Machines - 1
Memory availability - 5.4 GB per process on machine with least available
Fault Tolerance - 0 machines
Server time - 04/20/18 15:21:14
...
<prompt>fdb></prompt>
</screen>
</para>
<para>
You can also write programs using the available client libraries. For
example, the following Python program can be run in order to grab the
cluster status, as a quick example. (This example uses
<command>nix-shell</command> shebang support to automatically supply the
necessary Python modules).
<screen>
<prompt>a@link> </prompt>cat fdb-status.py
#! /usr/bin/env nix-shell
#! nix-shell -i python -p python pythonPackages.foundationdb52
import fdb
import json
def main():
fdb.api_version(520)
db = fdb.open()
@fdb.transactional
def get_status(tr):
return str(tr['\xff\xff/status/json'])
obj = json.loads(get_status(db))
print('FoundationDB available: %s' % obj['client']['database_status']['available'])
if __name__ == "__main__":
main()
<prompt>a@link> </prompt>chmod +x fdb-status.py
<prompt>a@link> </prompt>./fdb-status.py
FoundationDB available: True
<prompt>a@link></prompt>
</screen>
</para>
<para>
FoundationDB is run under the <command>foundationdb</command> user and group
by default, but this may be changed in the NixOS configuration. The systemd
unit <command>foundationdb.service</command> controls the
<command>fdbmonitor</command> process.
</para>
<para>
By default, the NixOS module for FoundationDB creates a single SSD-storage
based database for development and basic usage. This storage engine is
designed for SSDs and will perform poorly on HDDs; however it can handle far
more data than the alternative "memory" engine and is a better default
choice for most deployments. (Note that you can change the storage backend
on-the-fly for a given FoundationDB cluster using
<command>fdbcli</command>.)
</para>
<para>
Furthermore, only 1 server process and 1 backup agent are started in the
default configuration. See below for more on scaling to increase this.
</para>
<para>
FoundationDB stores all data for all server processes under
<filename>/var/lib/foundationdb</filename>. You can override this using
<option>services.foundationdb.dataDir</option>, e.g.
<programlisting>
services.foundationdb.dataDir = "/data/fdb";
</programlisting>
</para>
<para>
Similarly, logs are stored under <filename>/var/log/foundationdb</filename>
by default, and there is a corresponding
<option>services.foundationdb.logDir</option> as well.
</para>
</section>
<section xml:id="module-services-foundationdb-scaling">
<title>Scaling processes and backup agents</title>
<para>
Scaling the number of server processes is quite easy; simply specify
<option>services.foundationdb.serverProcesses</option> to be the number of
FoundationDB worker processes that should be started on the machine.
</para>
<para>
FoundationDB worker processes typically require 4GB of RAM per-process at
minimum for good performance, so this option is set to 1 by default since
the maximum amount of RAM is unknown. You're advised to abide by this
restriction, so pick a number of processes so that each has 4GB or more.
</para>
<para>
A similar option exists in order to scale backup agent processes,
<option>services.foundationdb.backupProcesses</option>. Backup agents are
not as performance/RAM sensitive, so feel free to experiment with the number
of available backup processes.
</para>
</section>
<section xml:id="module-services-foundationdb-clustering">
<title>Clustering</title>
<para>
FoundationDB on NixOS works similarly to other Linux systems, so this
section will be brief. Please refer to the full FoundationDB documentation
for more on clustering.
</para>
<para>
FoundationDB organizes clusters using a set of
<emphasis>coordinators</emphasis>, which are just specially-designated
worker processes. By default, every installation of FoundationDB on NixOS
will start as its own individual cluster, with a single coordinator: the
first worker process on <command>localhost</command>.
</para>
<para>
Coordinators are specified globally using the
<command>/etc/foundationdb/fdb.cluster</command> file, which all servers and
client applications will use to find and join coordinators. Note that this
file <emphasis>can not</emphasis> be managed by NixOS so easily:
FoundationDB is designed so that it will rewrite the file at runtime for all
clients and nodes when cluster coordinators change, with clients
transparently handling this without intervention. It is fundamentally a
mutable file, and you should not try to manage it in any way in NixOS.
</para>
<para>
When dealing with a cluster, there are two main things you want to do:
</para>
<itemizedlist>
<listitem>
<para>
Add a node to the cluster for storage/compute.
</para>
</listitem>
<listitem>
<para>
Promote an ordinary worker to a coordinator.
</para>
</listitem>
</itemizedlist>
<para>
A node must already be a member of the cluster in order to properly be
promoted to a coordinator, so you must always add it first if you wish to
promote it.
</para>
<para>
To add a machine to a FoundationDB cluster:
</para>
<itemizedlist>
<listitem>
<para>
Choose one of the servers to start as the initial coordinator.
</para>
</listitem>
<listitem>
<para>
Copy the <command>/etc/foundationdb/fdb.cluster</command> file from this
server to all the other servers. Restart FoundationDB on all of these
other servers, so they join the cluster.
</para>
</listitem>
<listitem>
<para>
All of these servers are now connected and working together in the
cluster, under the chosen coordinator.
</para>
</listitem>
</itemizedlist>
<para>
At this point, you can add as many nodes as you want by just repeating the
above steps. By default there will still be a single coordinator: you can
use <command>fdbcli</command> to change this and add new coordinators.
</para>
<para>
As a convenience, FoundationDB can automatically assign coordinators based
on the redundancy mode you wish to achieve for the cluster. Once all the
nodes have been joined, simply set the replication policy, and then issue
the <command>coordinators auto</command> command
</para>
<para>
For example, assuming we have 3 nodes available, we can enable double
redundancy mode, then auto-select coordinators. For double redundancy, 3
coordinators is ideal: therefore FoundationDB will make
<emphasis>every</emphasis> node a coordinator automatically:
</para>
<screen>
<prompt>fdbcli> </prompt>configure double ssd
<prompt>fdbcli> </prompt>coordinators auto
</screen>
<para>
This will transparently update all the servers within seconds, and
appropriately rewrite the <command>fdb.cluster</command> file, as well as
informing all client processes to do the same.
</para>
</section>
<section xml:id="module-services-foundationdb-connectivity">
<title>Client connectivity</title>
<para>
By default, all clients must use the current <command>fdb.cluster</command>
file to access a given FoundationDB cluster. This file is located by default
in <command>/etc/foundationdb/fdb.cluster</command> on all machines with the
FoundationDB service enabled, so you may copy the active one from your
cluster to a new node in order to connect, if it is not part of the cluster.
</para>
</section>
<section xml:id="module-services-foundationdb-authorization">
<title>Client authorization and TLS</title>
<para>
By default, any user who can connect to a FoundationDB process with the
correct cluster configuration can access anything. FoundationDB uses a
pluggable design to transport security, and out of the box it supports a
LibreSSL-based plugin for TLS support. This plugin not only does in-flight
encryption, but also performs client authorization based on the given
endpoint's certificate chain. For example, a FoundationDB server may be
configured to only accept client connections over TLS, where the client TLS
certificate is from organization <emphasis>Acme Co</emphasis> in the
<emphasis>Research and Development</emphasis> unit.
</para>
<para>
Configuring TLS with FoundationDB is done using the
<option>services.foundationdb.tls</option> options in order to control the
peer verification string, as well as the certificate and its private key.
</para>
<para>
Note that the certificate and its private key must be accessible to the
FoundationDB user account that the server runs under. These files are also
NOT managed by NixOS, as putting them into the store may reveal private
information.
</para>
<para>
After you have a key and certificate file in place, it is not enough to
simply set the NixOS module options -- you must also configure the
<command>fdb.cluster</command> file to specify that a given set of
coordinators use TLS. This is as simple as adding the suffix
<command>:tls</command> to your cluster coordinator configuration, after the
port number. For example, assuming you have a coordinator on localhost with
the default configuration, simply specifying:
</para>
<programlisting>
XXXXXX:XXXXXX@127.0.0.1:4500:tls
</programlisting>
<para>
will configure all clients and server processes to use TLS from now on.
</para>
</section>
<section xml:id="module-services-foundationdb-disaster-recovery">
<title>Backups and Disaster Recovery</title>
<para>
The usual rules for doing FoundationDB backups apply on NixOS as written in
the FoundationDB manual. However, one important difference is the security
profile for NixOS: by default, the <command>foundationdb</command> systemd
unit uses <emphasis>Linux namespaces</emphasis> to restrict write access to
the system, except for the log directory, data directory, and the
<command>/etc/foundationdb/</command> directory. This is enforced by default
and cannot be disabled.
</para>
<para>
However, a side effect of this is that the <command>fdbbackup</command>
command doesn't work properly for local filesystem backups: FoundationDB
uses a server process alongside the database processes to perform backups
and copy the backups to the filesystem. As a result, this process is put
under the restricted namespaces above: the backup process can only write to
a limited number of paths.
</para>
<para>
In order to allow flexible backup locations on local disks, the FoundationDB
NixOS module supports a
<option>services.foundationdb.extraReadWritePaths</option> option. This
option takes a list of paths, and adds them to the systemd unit, allowing
the processes inside the service to write (and read) the specified
directories.
</para>
<para>
For example, to create backups in <command>/opt/fdb-backups</command>, first
set up the paths in the module options:
</para>
<programlisting>
services.foundationdb.extraReadWritePaths = [ "/opt/fdb-backups" ];
</programlisting>
<para>
Restart the FoundationDB service, and it will now be able to write to this
directory (even if it does not yet exist.) Note: this path
<emphasis>must</emphasis> exist before restarting the unit. Otherwise,
systemd will not include it in the private FoundationDB namespace (and it
will not add it dynamically at runtime).
</para>
<para>
You can now perform a backup:
</para>
<screen>
<prompt>$ </prompt>sudo -u foundationdb fdbbackup start -t default -d file:///opt/fdb-backups
<prompt>$ </prompt>sudo -u foundationdb fdbbackup status -t default
</screen>
</section>
<section xml:id="module-services-foundationdb-limitations">
<title>Known limitations</title>
<para>
The FoundationDB setup for NixOS should currently be considered beta.
FoundationDB is not new software, but the NixOS compilation and integration
has only undergone fairly basic testing of all the available functionality.
</para>
<itemizedlist>
<listitem>
<para>
There is no way to specify individual parameters for individual
<command>fdbserver</command> processes. Currently, all server processes
inherit all the global <command>fdbmonitor</command> settings.
</para>
</listitem>
<listitem>
<para>
Ruby bindings are not currently installed.
</para>
</listitem>
<listitem>
<para>
Go bindings are not currently installed.
</para>
</listitem>
</itemizedlist>
</section>
<section xml:id="module-services-foundationdb-options">
<title>Options</title>
<para>
NixOS's FoundationDB module allows you to configure all of the most relevant
configuration options for <command>fdbmonitor</command>, matching it quite
closely. A complete list of options for the FoundationDB module may be found
<link linkend="opt-services.foundationdb.enable">here</link>. You should
also read the FoundationDB documentation as well.
</para>
</section>
<section xml:id="module-services-foundationdb-full-docs">
<title>Full documentation</title>
<para>
FoundationDB is a complex piece of software, and requires careful
administration to properly use. Full documentation for administration can be
found here: <link xlink:href="https://apple.github.io/foundationdb/"/>.
</para>
</section>
</chapter>

View file

@ -0,0 +1,149 @@
{ config, options, lib, pkgs, ... }:
with lib;
let
cfg = config.services.hbase;
opt = options.services.hbase;
buildProperty = configAttr:
(builtins.concatStringsSep "\n"
(lib.mapAttrsToList
(name: value: ''
<property>
<name>${name}</name>
<value>${builtins.toString value}</value>
</property>
'')
configAttr));
configFile = pkgs.writeText "hbase-site.xml"
''<configuration>
${buildProperty (opt.settings.default // cfg.settings)}
</configuration>
'';
configDir = pkgs.runCommand "hbase-config-dir" { preferLocalBuild = true; } ''
mkdir -p $out
cp ${cfg.package}/conf/* $out/
rm $out/hbase-site.xml
ln -s ${configFile} $out/hbase-site.xml
'' ;
in {
###### interface
options = {
services.hbase = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to run HBase.
'';
};
package = mkOption {
type = types.package;
default = pkgs.hbase;
defaultText = literalExpression "pkgs.hbase";
description = ''
HBase package to use.
'';
};
user = mkOption {
type = types.str;
default = "hbase";
description = ''
User account under which HBase runs.
'';
};
group = mkOption {
type = types.str;
default = "hbase";
description = ''
Group account under which HBase runs.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/hbase";
description = ''
Specifies location of HBase database files. This location should be
writable and readable for the user the HBase service runs as
(hbase by default).
'';
};
logDir = mkOption {
type = types.path;
default = "/var/log/hbase";
description = ''
Specifies the location of HBase log files.
'';
};
settings = mkOption {
type = with lib.types; attrsOf (oneOf [ str int bool ]);
default = {
"hbase.rootdir" = "file://${cfg.dataDir}/hbase";
"hbase.zookeeper.property.dataDir" = "${cfg.dataDir}/zookeeper";
};
defaultText = literalExpression ''
{
"hbase.rootdir" = "file://''${config.${opt.dataDir}}/hbase";
"hbase.zookeeper.property.dataDir" = "''${config.${opt.dataDir}}/zookeeper";
}
'';
description = ''
configurations in hbase-site.xml, see <link xlink:href="https://github.com/apache/hbase/blob/master/hbase-server/src/test/resources/hbase-site.xml"/> for details.
'';
};
};
};
###### implementation
config = mkIf config.services.hbase.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
"d '${cfg.logDir}' - ${cfg.user} ${cfg.group} - -"
];
systemd.services.hbase = {
description = "HBase Server";
wantedBy = [ "multi-user.target" ];
environment = {
# JRE 15 removed option `UseConcMarkSweepGC` which is needed.
JAVA_HOME = "${pkgs.jre8}";
HBASE_LOG_DIR = cfg.logDir;
};
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/hbase --config ${configDir} master start";
};
};
users.users.hbase = {
description = "HBase Server user";
group = "hbase";
uid = config.ids.uids.hbase;
};
users.groups.hbase.gid = config.ids.gids.hbase;
};
}

View file

@ -0,0 +1,197 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.influxdb;
configOptions = recursiveUpdate {
meta = {
bind-address = ":8088";
commit-timeout = "50ms";
dir = "${cfg.dataDir}/meta";
election-timeout = "1s";
heartbeat-timeout = "1s";
hostname = "localhost";
leader-lease-timeout = "500ms";
retention-autocreate = true;
};
data = {
dir = "${cfg.dataDir}/data";
wal-dir = "${cfg.dataDir}/wal";
max-wal-size = 104857600;
wal-enable-logging = true;
wal-flush-interval = "10m";
wal-partition-flush-delay = "2s";
};
cluster = {
shard-writer-timeout = "5s";
write-timeout = "5s";
};
retention = {
enabled = true;
check-interval = "30m";
};
http = {
enabled = true;
auth-enabled = false;
bind-address = ":8086";
https-enabled = false;
log-enabled = true;
pprof-enabled = false;
write-tracing = false;
};
monitor = {
store-enabled = false;
store-database = "_internal";
store-interval = "10s";
};
admin = {
enabled = true;
bind-address = ":8083";
https-enabled = false;
};
graphite = [{
enabled = false;
}];
udp = [{
enabled = false;
}];
collectd = [{
enabled = false;
typesdb = "${pkgs.collectd-data}/share/collectd/types.db";
database = "collectd_db";
bind-address = ":25826";
}];
opentsdb = [{
enabled = false;
}];
continuous_queries = {
enabled = true;
log-enabled = true;
recompute-previous-n = 2;
recompute-no-older-than = "10m";
compute-runs-per-interval = 10;
compute-no-more-than = "2m";
};
hinted-handoff = {
enabled = true;
dir = "${cfg.dataDir}/hh";
max-size = 1073741824;
max-age = "168h";
retry-rate-limit = 0;
retry-interval = "1s";
};
} cfg.extraConfig;
configFile = pkgs.runCommandLocal "config.toml" {
nativeBuildInputs = [ pkgs.remarshal ];
} ''
remarshal -if json -of toml \
< ${pkgs.writeText "config.json" (builtins.toJSON configOptions)} \
> $out
'';
in
{
###### interface
options = {
services.influxdb = {
enable = mkOption {
default = false;
description = "Whether to enable the influxdb server";
type = types.bool;
};
package = mkOption {
default = pkgs.influxdb;
defaultText = literalExpression "pkgs.influxdb";
description = "Which influxdb derivation to use";
type = types.package;
};
user = mkOption {
default = "influxdb";
description = "User account under which influxdb runs";
type = types.str;
};
group = mkOption {
default = "influxdb";
description = "Group under which influxdb runs";
type = types.str;
};
dataDir = mkOption {
default = "/var/db/influxdb";
description = "Data directory for influxd data files.";
type = types.path;
};
extraConfig = mkOption {
default = {};
description = "Extra configuration options for influxdb";
type = types.attrs;
};
};
};
###### implementation
config = mkIf config.services.influxdb.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];
systemd.services.influxdb = {
description = "InfluxDB Server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = ''${cfg.package}/bin/influxd -config "${configFile}"'';
User = cfg.user;
Group = cfg.group;
};
postStart =
let
scheme = if configOptions.http.https-enabled then "-k https" else "http";
bindAddr = (ba: if hasPrefix ":" ba then "127.0.0.1${ba}" else "${ba}")(toString configOptions.http.bind-address);
in
mkBefore ''
until ${pkgs.curl.bin}/bin/curl -s -o /dev/null ${scheme}://${bindAddr}/ping; do
sleep 1;
done
'';
};
users.users = optionalAttrs (cfg.user == "influxdb") {
influxdb = {
uid = config.ids.uids.influxdb;
group = "influxdb";
description = "Influxdb daemon user";
};
};
users.groups = optionalAttrs (cfg.group == "influxdb") {
influxdb.gid = config.ids.gids.influxdb;
};
};
}

View file

@ -0,0 +1,66 @@
{ config, lib, pkgs, ... }:
with lib;
let
format = pkgs.formats.json { };
cfg = config.services.influxdb2;
configFile = format.generate "config.json" cfg.settings;
in
{
options = {
services.influxdb2 = {
enable = mkEnableOption "the influxdb2 server";
package = mkOption {
default = pkgs.influxdb2-server;
defaultText = literalExpression "pkgs.influxdb2";
description = "influxdb2 derivation to use.";
type = types.package;
};
settings = mkOption {
default = { };
description = ''configuration options for influxdb2, see <link xlink:href="https://docs.influxdata.com/influxdb/v2.0/reference/config-options"/> for details.'';
type = format.type;
};
};
};
config = mkIf cfg.enable {
assertions = [{
assertion = !(builtins.hasAttr "bolt-path" cfg.settings) && !(builtins.hasAttr "engine-path" cfg.settings);
message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
}];
systemd.services.influxdb2 = {
description = "InfluxDB is an open-source, distributed, time series database";
documentation = [ "https://docs.influxdata.com/influxdb/" ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
INFLUXD_CONFIG_PATH = configFile;
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
StateDirectory = "influxdb2";
User = "influxdb2";
Group = "influxdb2";
CapabilityBoundingSet = "";
SystemCallFilter = "@system-service";
LimitNOFILE = 65536;
KillMode = "control-group";
Restart = "on-failure";
};
};
users.extraUsers.influxdb2 = {
isSystemUser = true;
group = "influxdb2";
};
users.extraGroups.influxdb2 = {};
};
meta.maintainers = with lib.maintainers; [ nickcao ];
}

View file

@ -0,0 +1,118 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.memcached;
memcached = pkgs.memcached;
in
{
###### interface
options = {
services.memcached = {
enable = mkEnableOption "Memcached";
user = mkOption {
type = types.str;
default = "memcached";
description = "The user to run Memcached as";
};
listen = mkOption {
type = types.str;
default = "127.0.0.1";
description = "The IP address to bind to.";
};
port = mkOption {
type = types.port;
default = 11211;
description = "The port to bind to.";
};
enableUnixSocket = mkEnableOption "unix socket at /run/memcached/memcached.sock";
maxMemory = mkOption {
type = types.ints.unsigned;
default = 64;
description = "The maximum amount of memory to use for storage, in megabytes.";
};
maxConnections = mkOption {
type = types.ints.unsigned;
default = 1024;
description = "The maximum number of simultaneous connections.";
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [];
description = "A list of extra options that will be added as a suffix when running memcached.";
};
};
};
###### implementation
config = mkIf config.services.memcached.enable {
users.users = optionalAttrs (cfg.user == "memcached") {
memcached.description = "Memcached server user";
memcached.isSystemUser = true;
memcached.group = "memcached";
};
users.groups = optionalAttrs (cfg.user == "memcached") { memcached = {}; };
environment.systemPackages = [ memcached ];
systemd.services.memcached = {
description = "Memcached server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart =
let
networking = if cfg.enableUnixSocket
then "-s /run/memcached/memcached.sock"
else "-l ${cfg.listen} -p ${toString cfg.port}";
in "${memcached}/bin/memcached ${networking} -m ${toString cfg.maxMemory} -c ${toString cfg.maxConnections} ${concatStringsSep " " cfg.extraOptions}";
User = cfg.user;
# Filesystem access
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RuntimeDirectory = "memcached";
# Caps
CapabilityBoundingSet = "";
NoNewPrivileges = true;
# Misc.
LockPersonality = true;
RestrictRealtime = true;
PrivateMounts = true;
MemoryDenyWriteExecute = true;
};
};
};
imports = [
(mkRemovedOptionModule ["services" "memcached" "socket"] ''
This option was replaced by a fixed unix socket path at /run/memcached/memcached.sock enabled using services.memcached.enableUnixSocket.
'')
];
}

View file

@ -0,0 +1,100 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.monetdb;
in {
meta.maintainers = with maintainers; [ StillerHarpo primeos ];
###### interface
options = {
services.monetdb = {
enable = mkEnableOption "the MonetDB database server";
package = mkOption {
type = types.package;
default = pkgs.monetdb;
defaultText = literalExpression "pkgs.monetdb";
description = "MonetDB package to use.";
};
user = mkOption {
type = types.str;
default = "monetdb";
description = "User account under which MonetDB runs.";
};
group = mkOption {
type = types.str;
default = "monetdb";
description = "Group under which MonetDB runs.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/monetdb";
description = "Data directory for the dbfarm.";
};
port = mkOption {
type = types.ints.u16;
default = 50000;
description = "Port to listen on.";
};
listenAddress = mkOption {
type = types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "Address to listen on.";
};
};
};
###### implementation
config = mkIf cfg.enable {
users.users.monetdb = mkIf (cfg.user == "monetdb") {
uid = config.ids.uids.monetdb;
group = cfg.group;
description = "MonetDB user";
home = cfg.dataDir;
createHome = true;
};
users.groups.monetdb = mkIf (cfg.group == "monetdb") {
gid = config.ids.gids.monetdb;
members = [ cfg.user ];
};
environment.systemPackages = [ cfg.package ];
systemd.services.monetdb = {
description = "MonetDB database server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [ cfg.package ];
unitConfig.RequiresMountsFor = "${cfg.dataDir}";
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/monetdbd start -n ${cfg.dataDir}";
ExecStop = "${cfg.package}/bin/monetdbd stop ${cfg.dataDir}";
};
preStart = ''
if [ ! -e ${cfg.dataDir}/.merovingian_properties ]; then
# Create the dbfarm (as cfg.user)
${cfg.package}/bin/monetdbd create ${cfg.dataDir}
fi
# Update the properties
${cfg.package}/bin/monetdbd set port=${toString cfg.port} ${cfg.dataDir}
${cfg.package}/bin/monetdbd set listenaddr=${cfg.listenAddress} ${cfg.dataDir}
'';
};
};
}

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