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:
commit
56de2bcd43
30691 changed files with 3076956 additions and 0 deletions
53
nixos/modules/services/admin/meshcentral.nix
Normal file
53
nixos/modules/services/admin/meshcentral.nix
Normal 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 ];
|
||||
}
|
||||
118
nixos/modules/services/admin/oxidized.nix
Normal file
118
nixos/modules/services/admin/oxidized.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
127
nixos/modules/services/admin/pgadmin.nix
Normal file
127
nixos/modules/services/admin/pgadmin.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
63
nixos/modules/services/admin/salt/master.nix
Normal file
63
nixos/modules/services/admin/salt/master.nix
Normal 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 ];
|
||||
}
|
||||
67
nixos/modules/services/admin/salt/minion.nix
Normal file
67
nixos/modules/services/admin/salt/minion.nix
Normal 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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
19
nixos/modules/services/amqp/activemq/ActiveMQBroker.java
Normal file
19
nixos/modules/services/amqp/activemq/ActiveMQBroker.java
Normal 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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
135
nixos/modules/services/amqp/activemq/default.nix
Normal file
135
nixos/modules/services/amqp/activemq/default.nix
Normal 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}"
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
228
nixos/modules/services/amqp/rabbitmq.nix
Normal file
228
nixos/modules/services/amqp/rabbitmq.nix
Normal 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
|
||||
''}
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
133
nixos/modules/services/audio/alsa.nix
Normal file
133
nixos/modules/services/audio/alsa.nix
Normal 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"; }
|
||||
];
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
115
nixos/modules/services/audio/botamusique.nix
Normal file
115
nixos/modules/services/audio/botamusique.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
142
nixos/modules/services/audio/hqplayerd.nix
Normal file
142
nixos/modules/services/audio/hqplayerd.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
131
nixos/modules/services/audio/icecast.nix
Normal file
131
nixos/modules/services/audio/icecast.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
294
nixos/modules/services/audio/jack.nix
Normal file
294
nixos/modules/services/audio/jack.nix
Normal 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 = [ ];
|
||||
}
|
||||
48
nixos/modules/services/audio/jmusicbot.nix
Normal file
48
nixos/modules/services/audio/jmusicbot.nix
Normal 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 ];
|
||||
}
|
||||
69
nixos/modules/services/audio/liquidsoap.nix
Normal file
69
nixos/modules/services/audio/liquidsoap.nix
Normal 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 );
|
||||
};
|
||||
|
||||
}
|
||||
108
nixos/modules/services/audio/mopidy.nix
Normal file
108
nixos/modules/services/audio/mopidy.nix
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
265
nixos/modules/services/audio/mpd.nix
Normal file
265
nixos/modules/services/audio/mpd.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
213
nixos/modules/services/audio/mpdscribble.nix
Normal file
213
nixos/modules/services/audio/mpdscribble.nix
Normal 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}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
71
nixos/modules/services/audio/navidrome.nix
Normal file
71
nixos/modules/services/audio/navidrome.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
19
nixos/modules/services/audio/networkaudiod.nix
Normal file
19
nixos/modules/services/audio/networkaudiod.nix
Normal 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" ];
|
||||
};
|
||||
}
|
||||
76
nixos/modules/services/audio/roon-bridge.nix
Normal file
76
nixos/modules/services/audio/roon-bridge.nix
Normal 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 {};
|
||||
};
|
||||
}
|
||||
79
nixos/modules/services/audio/roon-server.nix
Normal file
79
nixos/modules/services/audio/roon-server.nix
Normal 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 {};
|
||||
};
|
||||
}
|
||||
73
nixos/modules/services/audio/slimserver.nix
Normal file
73
nixos/modules/services/audio/slimserver.nix
Normal 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 = {};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
323
nixos/modules/services/audio/snapserver.nix
Normal file
323
nixos/modules/services/audio/snapserver.nix
Normal 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 ];
|
||||
};
|
||||
|
||||
}
|
||||
68
nixos/modules/services/audio/spotifyd.nix
Normal file
68
nixos/modules/services/audio/spotifyd.nix
Normal 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 ];
|
||||
}
|
||||
46
nixos/modules/services/audio/squeezelite.nix
Normal file
46
nixos/modules/services/audio/squeezelite.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
57
nixos/modules/services/audio/ympd.nix
Normal file
57
nixos/modules/services/audio/ympd.nix
Normal 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";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
119
nixos/modules/services/backup/automysqlbackup.nix
Normal file
119
nixos/modules/services/backup/automysqlbackup.nix
Normal 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"; };
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
578
nixos/modules/services/backup/bacula.nix
Normal file
578
nixos/modules/services/backup/bacula.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
730
nixos/modules/services/backup/borgbackup.nix
Normal file
730
nixos/modules/services/backup/borgbackup.nix
Normal 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);
|
||||
});
|
||||
}
|
||||
209
nixos/modules/services/backup/borgbackup.xml
Normal file
209
nixos/modules/services/backup/borgbackup.xml
Normal 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>
|
||||
58
nixos/modules/services/backup/borgmatic.nix
Normal file
58
nixos/modules/services/backup/borgmatic.nix
Normal 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 ];
|
||||
|
||||
};
|
||||
}
|
||||
244
nixos/modules/services/backup/btrbk.nix
Normal file
244
nixos/modules/services/backup/btrbk.nix
Normal 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);
|
||||
};
|
||||
|
||||
}
|
||||
86
nixos/modules/services/backup/duplicati.nix
Normal file
86
nixos/modules/services/backup/duplicati.nix
Normal 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;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
196
nixos/modules/services/backup/duplicity.nix
Normal file
196
nixos/modules/services/backup/duplicity.nix
Normal 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.")
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
130
nixos/modules/services/backup/mysql-backup.nix
Normal file
130
nixos/modules/services/backup/mysql-backup.nix
Normal 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} - - -"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
164
nixos/modules/services/backup/postgresql-backup.nix
Normal file
164
nixos/modules/services/backup/postgresql-backup.nix
Normal 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);
|
||||
})
|
||||
];
|
||||
|
||||
}
|
||||
204
nixos/modules/services/backup/postgresql-wal-receiver.nix
Normal file
204
nixos/modules/services/backup/postgresql-wal-receiver.nix
Normal 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 ];
|
||||
}
|
||||
111
nixos/modules/services/backup/restic-rest-server.nix
Normal file
111
nixos/modules/services/backup/restic-rest-server.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
334
nixos/modules/services/backup/restic.nix
Normal file
334
nixos/modules/services/backup/restic.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
75
nixos/modules/services/backup/rsnapshot.nix
Normal file
75
nixos/modules/services/backup/rsnapshot.nix
Normal 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;
|
||||
})
|
||||
]);
|
||||
}
|
||||
204
nixos/modules/services/backup/sanoid.nix
Normal file
204
nixos/modules/services/backup/sanoid.nix
Normal 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 ];
|
||||
}
|
||||
421
nixos/modules/services/backup/syncoid.nix
Normal file
421
nixos/modules/services/backup/syncoid.nix
Normal 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 ];
|
||||
}
|
||||
408
nixos/modules/services/backup/tarsnap.nix
Normal file
408
nixos/modules/services/backup/tarsnap.nix
Normal 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 ];
|
||||
};
|
||||
}
|
||||
125
nixos/modules/services/backup/tsm.nix
Normal file
125
nixos/modules/services/backup/tsm.nix
Normal 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 ];
|
||||
|
||||
}
|
||||
90
nixos/modules/services/backup/zfs-replication.nix
Normal file
90
nixos/modules/services/backup/zfs-replication.nix
Normal 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 ];
|
||||
};
|
||||
}
|
||||
469
nixos/modules/services/backup/znapzend.nix
Normal file
469
nixos/modules/services/backup/znapzend.nix
Normal 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::<facility> or <filepath>).
|
||||
'';
|
||||
};
|
||||
|
||||
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 ];
|
||||
}
|
||||
57
nixos/modules/services/backup/zrepl.nix
Normal file
57
nixos/modules/services/backup/zrepl.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
179
nixos/modules/services/blockchain/ethereum/geth.nix
Normal file
179
nixos/modules/services/blockchain/ethereum/geth.nix
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
112
nixos/modules/services/cluster/corosync/default.nix
Normal file
112
nixos/modules/services/cluster/corosync/default.nix
Normal 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}"
|
||||
'';
|
||||
};
|
||||
}
|
||||
44
nixos/modules/services/cluster/hadoop/conf.nix
Normal file
44
nixos/modules/services/cluster/hadoop/conf.nix
Normal 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}
|
||||
'')
|
||||
223
nixos/modules/services/cluster/hadoop/default.nix
Normal file
223
nixos/modules/services/cluster/hadoop/default.nix
Normal 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/";
|
||||
};
|
||||
};
|
||||
}
|
||||
204
nixos/modules/services/cluster/hadoop/hdfs.nix
Normal file
204
nixos/modules/services/cluster/hadoop/hdfs.nix
Normal 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;
|
||||
};
|
||||
})
|
||||
|
||||
];
|
||||
}
|
||||
200
nixos/modules/services/cluster/hadoop/yarn.nix
Normal file
200
nixos/modules/services/cluster/hadoop/yarn.nix
Normal 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 1024–65535 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;})
|
||||
];
|
||||
})
|
||||
|
||||
];
|
||||
}
|
||||
128
nixos/modules/services/cluster/k3s/default.nix
Normal file
128
nixos/modules/services/cluster/k3s/default.nix
Normal 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 ]
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
171
nixos/modules/services/cluster/kubernetes/addon-manager.nix
Normal file
171
nixos/modules/services/cluster/kubernetes/addon-manager.nix
Normal 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;
|
||||
}
|
||||
368
nixos/modules/services/cluster/kubernetes/addons/dns.nix
Normal file
368
nixos/modules/services/cluster/kubernetes/addons/dns.nix
Normal 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;
|
||||
}
|
||||
500
nixos/modules/services/cluster/kubernetes/apiserver.nix
Normal file
500
nixos/modules/services/cluster/kubernetes/apiserver.nix
Normal 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;
|
||||
}
|
||||
176
nixos/modules/services/cluster/kubernetes/controller-manager.nix
Normal file
176
nixos/modules/services/cluster/kubernetes/controller-manager.nix
Normal 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;
|
||||
}
|
||||
315
nixos/modules/services/cluster/kubernetes/default.nix
Normal file
315
nixos/modules/services/cluster/kubernetes/default.nix
Normal 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;
|
||||
}
|
||||
100
nixos/modules/services/cluster/kubernetes/flannel.nix
Normal file
100
nixos/modules/services/cluster/kubernetes/flannel.nix
Normal 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;
|
||||
}
|
||||
398
nixos/modules/services/cluster/kubernetes/kubelet.nix
Normal file
398
nixos/modules/services/cluster/kubernetes/kubelet.nix
Normal 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;
|
||||
}
|
||||
406
nixos/modules/services/cluster/kubernetes/pki.nix
Normal file
406
nixos/modules/services/cluster/kubernetes/pki.nix
Normal 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/<path>).
|
||||
'';
|
||||
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;
|
||||
}
|
||||
102
nixos/modules/services/cluster/kubernetes/proxy.nix
Normal file
102
nixos/modules/services/cluster/kubernetes/proxy.nix
Normal 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;
|
||||
}
|
||||
101
nixos/modules/services/cluster/kubernetes/scheduler.nix
Normal file
101
nixos/modules/services/cluster/kubernetes/scheduler.nix
Normal 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;
|
||||
}
|
||||
52
nixos/modules/services/cluster/pacemaker/default.nix
Normal file
52
nixos/modules/services/cluster/pacemaker/default.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
162
nixos/modules/services/cluster/spark/default.nix
Normal file
162
nixos/modules/services/cluster/spark/default.nix
Normal 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 = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
131
nixos/modules/services/computing/boinc/client.nix
Normal file
131
nixos/modules/services/computing/boinc/client.nix
Normal 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];
|
||||
};
|
||||
}
|
||||
91
nixos/modules/services/computing/foldingathome/client.nix
Normal file
91
nixos/modules/services/computing/foldingathome/client.nix
Normal 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 ];
|
||||
};
|
||||
}
|
||||
443
nixos/modules/services/computing/slurm/slurm.nix
Normal file
443
nixos/modules/services/computing/slurm/slurm.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
63
nixos/modules/services/computing/torque/mom.nix
Normal file
63
nixos/modules/services/computing/torque/mom.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
96
nixos/modules/services/computing/torque/server.nix
Normal file
96
nixos/modules/services/computing/torque/server.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
|
@ -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 you’ll 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 ];
|
||||
}
|
||||
|
|
@ -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; [ ];
|
||||
|
||||
}
|
||||
|
|
@ -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.<name>.hooks</option>";
|
||||
description = ''
|
||||
Path to the directory storing the hooks.
|
||||
Consider using <option>services.buildkite-agents.<name>.hooks.<name></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.")
|
||||
];
|
||||
}
|
||||
320
nixos/modules/services/continuous-integration/github-runner.nix
Normal file
320
nixos/modules/services/continuous-integration/github-runner.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
588
nixos/modules/services/continuous-integration/gitlab-runner.nix
Normal file
588
nixos/modules/services/continuous-integration/gitlab-runner.nix
Normal 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 (<host>:<port>) 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=<CI server URL></literal>
|
||||
|
||||
<literal>REGISTRATION_TOKEN=<registration secret></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 can’t 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" )
|
||||
];
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
61
nixos/modules/services/continuous-integration/hail.nix
Normal file
61
nixos/modules/services/continuous-integration/hail.nix
Normal 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}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
}
|
||||
505
nixos/modules/services/continuous-integration/hydra/default.nix
Normal file
505
nixos/modules/services/continuous-integration/hydra/default.nix
Normal 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
|
||||
'';
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
156
nixos/modules/services/databases/aerospike.nix
Normal file
156
nixos/modules/services/databases/aerospike.nix
Normal 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"
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
563
nixos/modules/services/databases/cassandra.nix
Normal file
563
nixos/modules/services/databases/cassandra.nix
Normal 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 ];
|
||||
}
|
||||
78
nixos/modules/services/databases/clickhouse.nix
Normal file
78
nixos/modules/services/databases/clickhouse.nix
Normal 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";
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
225
nixos/modules/services/databases/cockroachdb.nix
Normal file
225
nixos/modules/services/databases/cockroachdb.nix
Normal 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 ];
|
||||
}
|
||||
231
nixos/modules/services/databases/couchdb.nix
Normal file
231
nixos/modules/services/databases/couchdb.nix
Normal 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;
|
||||
|
||||
};
|
||||
}
|
||||
152
nixos/modules/services/databases/dragonflydb.nix
Normal file
152
nixos/modules/services/databases/dragonflydb.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
168
nixos/modules/services/databases/firebird.nix
Normal file
168
nixos/modules/services/databases/firebird.nix
Normal 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;
|
||||
|
||||
};
|
||||
}
|
||||
429
nixos/modules/services/databases/foundationdb.nix
Normal file
429
nixos/modules/services/databases/foundationdb.nix
Normal 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 ];
|
||||
}
|
||||
443
nixos/modules/services/databases/foundationdb.xml
Normal file
443
nixos/modules/services/databases/foundationdb.xml
Normal 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>
|
||||
149
nixos/modules/services/databases/hbase.nix
Normal file
149
nixos/modules/services/databases/hbase.nix
Normal 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;
|
||||
|
||||
};
|
||||
}
|
||||
197
nixos/modules/services/databases/influxdb.nix
Normal file
197
nixos/modules/services/databases/influxdb.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
66
nixos/modules/services/databases/influxdb2.nix
Normal file
66
nixos/modules/services/databases/influxdb2.nix
Normal 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 ];
|
||||
}
|
||||
118
nixos/modules/services/databases/memcached.nix
Normal file
118
nixos/modules/services/databases/memcached.nix
Normal 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.
|
||||
'')
|
||||
];
|
||||
|
||||
}
|
||||
100
nixos/modules/services/databases/monetdb.nix
Normal file
100
nixos/modules/services/databases/monetdb.nix
Normal 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
Loading…
Add table
Add a link
Reference in a new issue