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

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

View File

@@ -0,0 +1,149 @@
{
config,
lib,
pkgs,
...
}:
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 = lib.mkEnableOption "Aerospike server";
package = lib.mkPackageOption pkgs "aerospike" { };
workDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/aerospike";
description = "Location where Aerospike stores its files";
};
networkConfig = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
namespace test {
replication-factor 2
memory-size 4G
default-ttl 30d
storage-engine memory
}
'';
description = "Extra configuration";
};
};
};
###### implementation
config = lib.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;
boot.kernel.sysctl = {
"net.core.rmem_max" = lib.mkDefault 15728640;
"net.core.wmem_max" = lib.mkDefault 5242880;
};
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
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}"
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/smd"
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/udf"
install -d -m0700 -o ${serviceConfig.User} -g ${serviceConfig.Group} "${cfg.workDir}/udf/lua"
'';
};
};
}

View File

@@ -0,0 +1,578 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
concatStringsSep
flip
literalMD
literalExpression
optionalAttrs
optionals
recursiveUpdate
mkEnableOption
mkPackageOption
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";
hints_directory = "${cfg.homeDir}/hints";
}
// optionalAttrs (cfg.seedAddresses != [ ]) {
seed_provider = [
{
class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
parameters = [ { seeds = concatStringsSep "," cfg.seedAddresses; } ];
}
];
}
);
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 package;
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"
cp $package/conf/jvm*.options $out/
'';
};
defaultJmxRolesFile = builtins.foldl' (left: right: left + right) "" (
map (role: "${role.username} ${role.password}") cfg.jmxRoles
);
fullJvmOptions =
cfg.jvmOpts
++ [
# Historically, we don't use a log dir, whereas the upstream scripts do
# expect this. We override those by providing our own -Xlog:gc flag.
"-Xlog:gc=warning,heap*=warning,age*=warning,safepoint=warning,promotion*=warning"
]
++ 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}"
];
commonEnv = {
# Sufficient for cassandra 2.x, 3.x
CASSANDRA_CONF = "${cassandraEtc}";
# Required since cassandra 4
CASSANDRA_LOGBACK_CONF = "${cassandraEtc}/logback.xml";
};
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 = mkPackageOption pkgs "cassandra" {
example = "cassandra_4";
};
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 {option}`listenAddress` OR {option}`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 {option}`listenAddress` 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 {option}`rpcAddress` OR {option}`rpcInterface`, not both.
Leaving {option}`rpcAddress` blank has the same effect as on
{option}`listenAddress` (i.e. it will be based on the configured hostname
of the node).
Note that unlike {option}`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 {option}`rpcAddress` OR {option}`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 {option}`rpcAddress`,
but the port it uses is not `rpc_port` but
`native_transport_port`. See the official Cassandra
docs for more information on these variables and set them using
{option}`extraConfig`.
'';
};
extraConfig = mkOption {
type = types.attrs;
default = { };
example = {
commitlog_sync_batch_window_in_ms = 3;
};
description = ''
Extra options to be merged into {file}`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 {file}`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.
{command}`nodetool repair --full` is executed. See
<https://cassandra.apache.org/doc/latest/operating/repair.html>
for more information.
Set to `null` 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.
{command}`nodetool repair` is executed. See
<https://cassandra.apache.org/doc/latest/operating/repair.html>
for more information.
Set to `null` 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 {option}`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 {option}`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.port;
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. {command}`nodetool`)
BEWARE: The passwords will be stored world readable in the nix store.
It's recommended to use your own protected file using
{option}`jmxRolesFile`
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 = pkgs.writeText "jmx-roles-file" defaultJmxRolesFile;
defaultText = "generated configuration file";
example = "/var/lib/cassandra/jmx.password";
description = ''
Specify your own jmx roles file.
'';
};
};
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>.
'';
}
];
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 = commonEnv // {
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" ];
environment = commonEnv;
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" ];
environment = commonEnv;
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = concatStringsSep " " (
[
"${cfg.package}/bin/nodetool"
"repair"
]
++ cfg.incrementalRepairOptions
);
};
};
systemd.timers.cassandra-incremental-repair = mkIf (cfg.incrementalRepairInterval != null) {
description = "Schedule incremental repairs on Cassandra";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = cfg.incrementalRepairInterval;
OnUnitActiveSec = cfg.incrementalRepairInterval;
Persistent = true;
};
};
};
meta.maintainers = with lib.maintainers; [ roberth ];
}

View File

@@ -0,0 +1,99 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.chromadb;
inherit (lib)
mkEnableOption
mkPackageOption
mkOption
mkIf
types
;
in
{
meta.maintainers = [ ];
imports = [
(lib.mkRemovedOptionModule [ "services" "chromadb" "logFile" ] ''
ChromaDB has removed the --log-path parameter that logFile relied on.
'')
];
options = {
services.chromadb = {
enable = mkEnableOption "ChromaDB, an open-source AI application database.";
package = mkPackageOption pkgs [ "python3Packages" "chromadb" ] { };
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Defines the IP address by which ChromaDB will be accessible.
'';
};
port = mkOption {
type = types.port;
default = 8000;
description = ''
Defined the port number to listen.
'';
};
dbpath = mkOption {
type = types.str;
default = "/var/lib/chromadb";
description = "Location where ChromaDB stores its files";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Whether to automatically open the specified TCP port in the firewall.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.chromadb = {
description = "ChromaDB";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
StateDirectory = "chromadb";
WorkingDirectory = "/var/lib/chromadb";
LogsDirectory = "chromadb";
ExecStart = "${lib.getExe cfg.package} run --path ${cfg.dbpath} --host ${cfg.host} --port ${toString cfg.port}";
Restart = "on-failure";
ProtectHome = true;
ProtectSystem = "strict";
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
NoNewPrivileges = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
DynamicUser = true;
};
};
networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ];
};
}

View File

@@ -0,0 +1,81 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.clickhouse;
in
{
###### interface
options = {
services.clickhouse = {
enable = lib.mkEnableOption "ClickHouse database server";
package = lib.mkPackageOption pkgs "clickhouse" { };
};
};
###### implementation
config = lib.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 = {
Type = "notify";
User = "clickhouse";
Group = "clickhouse";
ConfigurationDirectory = "clickhouse-server";
AmbientCapabilities = "CAP_SYS_NICE";
StateDirectory = "clickhouse";
LogsDirectory = "clickhouse";
ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=/etc/clickhouse-server/config.xml";
TimeoutStartSec = "infinity";
};
environment = {
# Switching off watchdog is very important for sd_notify to work correctly.
CLICKHOUSE_WATCHDOG_ENABLE = "0";
};
};
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 = lib.mkDefault "UTC";
};
}

View File

@@ -0,0 +1,236 @@
{
config,
lib,
pkgs,
utils,
...
}:
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 = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "Address to bind to for ${descr}";
};
port = lib.mkOption {
type = lib.types.port;
default = defaultPort;
description = "Port to bind to for ${descr}";
};
};
in
{
options = {
services.cockroachdb = {
enable = lib.mkEnableOption "CockroachDB Server";
listen = addressOption "intra-cluster communication" 26257;
http = addressOption "http-based Admin UI" 8080;
locality = lib.mkOption {
type = lib.types.nullOr lib.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:
```
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
```
'';
};
join = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "The addresses for connecting the node to a cluster.";
};
insecure = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run in insecure mode.";
};
certsDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "The path to the certificate directory.";
};
user = lib.mkOption {
type = lib.types.str;
default = "cockroachdb";
description = "User account under which CockroachDB runs";
};
group = lib.mkOption {
type = lib.types.str;
default = "cockroachdb";
description = "User account under which CockroachDB runs";
};
openPorts = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open firewall ports for cluster communication by default";
};
cache = lib.mkOption {
type = lib.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,
`"25%"`, `"0.25"` both represent
25% of the available system memory. The values
`"1000000000"` and `"1GB"` both
represent 1 gigabyte of memory.
'';
};
maxSqlMemory = lib.mkOption {
type = lib.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,
`"25%"`, `"0.25"` both represent
25% of the available system memory. The values
`"1000000000"` and `"1GB"` both
represent 1 gigabyte of memory.
'';
};
package = lib.mkPackageOption pkgs "cockroachdb" {
extraDescription = ''
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 = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"--advertise-addr"
"[fe80::f6f2:::]"
];
description = ''
Extra CLI arguments passed to {command}`cockroach start`.
For the full list of supported arguments, check <https://www.cockroachlabs.com/docs/stable/cockroach-start.html#flags>
'';
};
};
};
config = lib.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 = lib.optionalAttrs (cfg.user == "cockroachdb") {
cockroachdb = {
description = "CockroachDB Server User";
uid = config.ids.uids.cockroachdb;
group = cfg.group;
};
};
users.groups = lib.optionalAttrs (cfg.group == "cockroachdb") {
cockroachdb.gid = config.ids.gids.cockroachdb;
};
networking.firewall.allowedTCPPorts = lib.optionals cfg.openPorts [
cfg.http.port
cfg.listen.port
];
systemd.services.cockroachdb = {
description = "CockroachDB Server";
documentation = [
"man:cockroach(1)"
"https://www.cockroachlabs.com"
];
after = [
"network.target"
"time-sync.target"
];
requires = [ "time-sync.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = "/var/lib/cockroachdb";
serviceConfig = {
ExecStart = startupCommand;
Type = "notify";
User = cfg.user;
StateDirectory = "cockroachdb";
StateDirectoryMode = "0700";
Restart = "always";
# A conservative-ish timeout is alright here, because for Type=notify
# cockroach will send systemd pings during startup to keep it alive
TimeoutStopSec = 60;
RestartSec = 10;
};
};
};
meta.maintainers = with lib.maintainers; [ thoughtpolice ];
}

View File

@@ -0,0 +1,229 @@
{
config,
options,
lib,
pkgs,
...
}:
let
cfg = config.services.couchdb;
opt = options.services.couchdb;
baseConfig = {
couchdb = {
database_dir = cfg.databaseDir;
uri_file = cfg.uriFile;
view_index_dir = cfg.viewIndexDir;
};
chttpd = {
port = cfg.port;
bind_address = cfg.bindAddress;
};
log = {
file = cfg.logFile;
};
};
adminConfig = lib.optionalAttrs (cfg.adminPass != null) {
admins = {
"${cfg.adminUser}" = cfg.adminPass;
};
};
appConfig = lib.recursiveUpdate (lib.recursiveUpdate baseConfig adminConfig) cfg.extraConfig;
optionsConfigFile = pkgs.writeText "couchdb.ini" (lib.generators.toINI { } appConfig);
# we are actually specifying 5 configuration files:
# 1. the preinstalled default.ini
# 2. the module configuration
# 3. the extraConfigFiles from the module options
# 4. the locally writable config file, which couchdb itself writes to
configFiles = [
"${cfg.package}/etc/default.ini"
optionsConfigFile
]
++ cfg.extraConfigFiles
++ [ cfg.configFile ];
executable = "${cfg.package}/bin/couchdb";
in
{
###### interface
options = {
services.couchdb = {
enable = lib.mkEnableOption "CouchDB Server";
package = lib.mkPackageOption pkgs "couchdb3" { };
adminUser = lib.mkOption {
type = lib.types.str;
default = "admin";
description = ''
Couchdb (i.e. fauxton) account with permission for all dbs and
tasks.
'';
};
adminPass = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Couchdb (i.e. fauxton) account with permission for all dbs and
tasks.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "couchdb";
description = ''
User account under which couchdb runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "couchdb";
description = ''
Group account under which couchdb runs.
'';
};
# couchdb options: https://docs.couchdb.org/en/latest/config/index.html
databaseDir = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Defines the IP address by which CouchDB will be accessible.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 5984;
description = ''
Defined the port number to listen.
'';
};
logFile = lib.mkOption {
type = lib.types.path;
default = "/var/log/couchdb.log";
description = ''
Specifies the location of file for logging output.
'';
};
extraConfig = lib.mkOption {
type = lib.types.attrs;
default = { };
description = "Extra configuration options for CouchDB";
};
extraConfigFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
Extra configuration files. Overrides any other configuration. You can use this to setup the Admin user without putting the password in your nix store.
'';
};
argsFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.package}/etc/vm.args";
defaultText = lib.literalExpression ''"config.${opt.package}/etc/vm.args"'';
description = ''
vm.args configuration. Overrides Couchdb's Erlang VM parameters file.
'';
};
configFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/couchdb/local.ini";
description = ''
Configuration file for persisting runtime changes. File
needs to be readable and writable from couchdb user/group.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
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 = {
ERL_FLAGS = ''-couch_ini ${lib.concatStringsSep " " configFiles}'';
# 5. the vm.args file
COUCHDB_ARGS_FILE = ''${cfg.argsFile}'';
HOME = ''${cfg.databaseDir}'';
};
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = executable;
};
};
users.users.couchdb = {
description = "CouchDB Server user";
group = "couchdb";
uid = config.ids.uids.couchdb;
};
users.groups.couchdb.gid = config.ids.gids.couchdb;
};
}

View File

@@ -0,0 +1,167 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.dgraph;
settingsFormat = pkgs.formats.json { };
configFile = settingsFormat.generate "config.json" cfg.settings;
dgraphWithNode =
pkgs.runCommand "dgraph"
{
nativeBuildInputs = [ pkgs.makeWrapper ];
}
''
mkdir -p $out/bin
makeWrapper ${cfg.package}/bin/dgraph $out/bin/dgraph \
--prefix PATH : "${lib.makeBinPath [ pkgs.nodejs ]}" \
'';
securityOptions = {
NoNewPrivileges = true;
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = "";
LockPersonality = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@cpu-emulation"
"~@debug"
"~@keyring"
"~@memlock"
"~@obsolete"
"~@privileged"
"~@setuid"
];
};
in
{
options = {
services.dgraph = {
enable = lib.mkEnableOption "Dgraph native GraphQL database with a graph backend";
package = lib.mkPackageOption pkgs "dgraph" { };
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Contents of the dgraph config. For more details see <https://dgraph.io/docs/deploy/config>
'';
};
alpha = {
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
The host which dgraph alpha will be run on.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 7080;
description = ''
The port which to run dgraph alpha on.
'';
};
};
zero = {
host = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
The host which dgraph zero will be run on.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 5080;
description = ''
The port which to run dgraph zero on.
'';
};
};
};
};
config = lib.mkIf cfg.enable {
services.dgraph.settings = {
badger.compression = lib.mkDefault "zstd:3";
};
systemd.services.dgraph-zero = {
description = "Dgraph native GraphQL database with a graph backend. Zero controls node clustering";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "dgraph-zero";
WorkingDirectory = "/var/lib/dgraph-zero";
DynamicUser = true;
ExecStart = "${cfg.package}/bin/dgraph zero --my ${cfg.zero.host}:${toString cfg.zero.port}";
Restart = "on-failure";
}
// securityOptions;
};
systemd.services.dgraph-alpha = {
description = "Dgraph native GraphQL database with a graph backend. Alpha serves data";
after = [
"network.target"
"dgraph-zero.service"
];
requires = [ "dgraph-zero.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "dgraph-alpha";
WorkingDirectory = "/var/lib/dgraph-alpha";
DynamicUser = true;
ExecStart = "${dgraphWithNode}/bin/dgraph alpha --config ${configFile} --my ${cfg.alpha.host}:${toString cfg.alpha.port} --zero ${cfg.zero.host}:${toString cfg.zero.port}";
ExecStop = ''
${pkgs.curl}/bin/curl --data "mutation { shutdown { response { message code } } }" \
--header 'Content-Type: application/graphql' \
-X POST \
http://localhost:8080/admin
'';
Restart = "on-failure";
}
// securityOptions;
};
};
meta.maintainers = with lib.maintainers; [ happysalada ];
}

View File

@@ -0,0 +1,158 @@
{
config,
lib,
pkgs,
...
}:
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 = lib.mkEnableOption "DragonflyDB";
user = lib.mkOption {
type = lib.types.str;
default = "dragonfly";
description = "The user to run DragonflyDB as";
};
port = lib.mkOption {
type = lib.types.port;
default = 6379;
description = "The TCP port to accept connections.";
};
bind = lib.mkOption {
type = with lib.types; nullOr str;
default = "127.0.0.1";
description = ''
The IP interface to bind to.
`null` means "all interfaces".
'';
};
requirePass = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Password for database";
example = "letmein!";
};
maxMemory = lib.mkOption {
type = with lib.types; nullOr ints.unsigned;
default = null;
description = ''
The maximum amount of memory to use for storage (in bytes).
`null` means this will be automatically set.
'';
};
memcachePort = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
description = ''
To enable memcached compatible API on this port.
`null` means disabled.
'';
};
keysOutputLimit = lib.mkOption {
type = lib.types.ints.unsigned;
default = 8192;
description = ''
Maximum number of returned keys in keys command.
`keys` is a dangerous command.
We truncate its result to avoid blowup in memory when fetching too many keys.
'';
};
dbNum = lib.mkOption {
type = with lib.types; nullOr ints.unsigned;
default = null;
description = "Maximum number of supported databases for `select`";
};
cacheMode = lib.mkOption {
type = with lib.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 = lib.mkIf config.services.dragonflydb.enable {
users.users = lib.optionalAttrs (cfg.user == "dragonfly") {
dragonfly.description = "DragonflyDB server user";
dragonfly.isSystemUser = true;
dragonfly.group = "dragonfly";
};
users.groups = lib.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 ${
lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "--${n} ${lib.escapeShellArg v}") settings)
}";
User = cfg.user;
# Filesystem access
ReadWritePaths = [ settings.dir ];
StateDirectory = "dragonflydb";
StateDirectoryMode = "0700";
# Process Properties
LimitMEMLOCK = "infinity";
# Caps
CapabilityBoundingSet = "";
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
LockPersonality = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictRealtime = true;
PrivateMounts = true;
MemoryDenyWriteExecute = true;
};
};
};
}

View File

@@ -0,0 +1,246 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.etcd;
opt = options.services.etcd;
in
{
options.services.etcd = {
enable = lib.mkOption {
description = "Whether to enable etcd.";
default = false;
type = lib.types.bool;
};
package = lib.mkPackageOption pkgs "etcd" { };
name = lib.mkOption {
description = "Etcd unique node name.";
default = config.networking.hostName;
defaultText = lib.literalExpression "config.networking.hostName";
type = lib.types.str;
};
advertiseClientUrls = lib.mkOption {
description = "Etcd list of this member's client URLs to advertise to the rest of the cluster.";
default = cfg.listenClientUrls;
defaultText = lib.literalExpression "config.${opt.listenClientUrls}";
type = lib.types.listOf lib.types.str;
};
listenClientUrls = lib.mkOption {
description = "Etcd list of URLs to listen on for client traffic.";
default = [ "http://127.0.0.1:2379" ];
type = lib.types.listOf lib.types.str;
};
listenPeerUrls = lib.mkOption {
description = "Etcd list of URLs to listen on for peer traffic.";
default = [ "http://127.0.0.1:2380" ];
type = lib.types.listOf lib.types.str;
};
initialAdvertisePeerUrls = lib.mkOption {
description = "Etcd list of this member's peer URLs to advertise to rest of the cluster.";
default = cfg.listenPeerUrls;
defaultText = lib.literalExpression "config.${opt.listenPeerUrls}";
type = lib.types.listOf lib.types.str;
};
initialCluster = lib.mkOption {
description = "Etcd initial cluster configuration for bootstrapping.";
default = [ "${cfg.name}=http://127.0.0.1:2380" ];
defaultText = lib.literalExpression ''["''${config.${opt.name}}=http://127.0.0.1:2380"]'';
type = lib.types.listOf lib.types.str;
};
initialClusterState = lib.mkOption {
description = "Etcd initial cluster configuration for bootstrapping.";
default = "new";
type = lib.types.enum [
"new"
"existing"
];
};
initialClusterToken = lib.mkOption {
description = "Etcd initial cluster token for etcd cluster during bootstrap.";
default = "etcd-cluster";
type = lib.types.str;
};
discovery = lib.mkOption {
description = "Etcd discovery url";
default = "";
type = lib.types.str;
};
clientCertAuth = lib.mkOption {
description = "Whether to use certs for client authentication";
default = false;
type = lib.types.bool;
};
trustedCaFile = lib.mkOption {
description = "Certificate authority file to use for clients";
default = null;
type = lib.types.nullOr lib.types.path;
};
certFile = lib.mkOption {
description = "Cert file to use for clients";
default = null;
type = lib.types.nullOr lib.types.path;
};
keyFile = lib.mkOption {
description = "Key file to use for clients";
default = null;
type = lib.types.nullOr lib.types.path;
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Open etcd ports in the firewall.
Ports opened:
- 2379/tcp for client requests
- 2380/tcp for peer communication
'';
};
peerCertFile = lib.mkOption {
description = "Cert file to use for peer to peer communication";
default = cfg.certFile;
defaultText = lib.literalExpression "config.${opt.certFile}";
type = lib.types.nullOr lib.types.path;
};
peerKeyFile = lib.mkOption {
description = "Key file to use for peer to peer communication";
default = cfg.keyFile;
defaultText = lib.literalExpression "config.${opt.keyFile}";
type = lib.types.nullOr lib.types.path;
};
peerTrustedCaFile = lib.mkOption {
description = "Certificate authority file to use for peer to peer communication";
default = cfg.trustedCaFile;
defaultText = lib.literalExpression "config.${opt.trustedCaFile}";
type = lib.types.nullOr lib.types.path;
};
peerClientCertAuth = lib.mkOption {
description = "Whether to check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA";
default = false;
type = lib.types.bool;
};
extraConf = lib.mkOption {
description = ''
Etcd extra configuration. See
<https://github.com/coreos/etcd/blob/master/Documentation/op-guide/configuration.md#configuration-flags>
'';
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''
{
"CORS" = "*";
"NAME" = "default-name";
"MAX_RESULT_BUFFER" = "1024";
"MAX_CLUSTER_SIZE" = "9";
"MAX_RETRY_ATTEMPTS" = "3";
}
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/etcd";
description = "Etcd data directory.";
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.settings."10-etcd".${cfg.dataDir}.d = {
user = "etcd";
mode = "0700";
};
systemd.services.etcd = {
description = "etcd key-value store";
wantedBy = [ "multi-user.target" ];
after = [
"network-online.target"
]
++ lib.optional config.networking.firewall.enable "firewall.service";
wants = [
"network-online.target"
]
++ lib.optional config.networking.firewall.enable "firewall.service";
environment =
(lib.filterAttrs (n: v: v != null) {
ETCD_NAME = cfg.name;
ETCD_DISCOVERY = cfg.discovery;
ETCD_DATA_DIR = cfg.dataDir;
ETCD_ADVERTISE_CLIENT_URLS = lib.concatStringsSep "," cfg.advertiseClientUrls;
ETCD_LISTEN_CLIENT_URLS = lib.concatStringsSep "," cfg.listenClientUrls;
ETCD_LISTEN_PEER_URLS = lib.concatStringsSep "," cfg.listenPeerUrls;
ETCD_INITIAL_ADVERTISE_PEER_URLS = lib.concatStringsSep "," cfg.initialAdvertisePeerUrls;
ETCD_PEER_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
ETCD_PEER_TRUSTED_CA_FILE = cfg.peerTrustedCaFile;
ETCD_PEER_CERT_FILE = cfg.peerCertFile;
ETCD_PEER_KEY_FILE = cfg.peerKeyFile;
ETCD_CLIENT_CERT_AUTH = toString cfg.clientCertAuth;
ETCD_TRUSTED_CA_FILE = cfg.trustedCaFile;
ETCD_CERT_FILE = cfg.certFile;
ETCD_KEY_FILE = cfg.keyFile;
})
// (lib.optionalAttrs (cfg.discovery == "") {
ETCD_INITIAL_CLUSTER = lib.concatStringsSep "," cfg.initialCluster;
ETCD_INITIAL_CLUSTER_STATE = cfg.initialClusterState;
ETCD_INITIAL_CLUSTER_TOKEN = cfg.initialClusterToken;
})
// (lib.mapAttrs' (n: v: lib.nameValuePair "ETCD_${n}" v) cfg.extraConf);
unitConfig = {
Documentation = "https://github.com/coreos/etcd";
};
serviceConfig = {
Type = "notify";
Restart = "always";
RestartSec = "30s";
ExecStart = "${cfg.package}/bin/etcd";
User = "etcd";
LimitNOFILE = 40000;
};
};
environment.systemPackages = [ cfg.package ];
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
2379 # for client requests
2380 # for peer communication
];
};
users.users.etcd = {
isSystemUser = true;
group = "etcd";
description = "Etcd daemon user";
home = cfg.dataDir;
};
users.groups.etcd = { };
};
}

View File

@@ -0,0 +1,108 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.ferretdb;
in
{
meta.maintainers = with lib.maintainers; [
julienmalka
camillemndn
];
options = {
services.ferretdb = {
enable = lib.mkEnableOption "FerretDB, an Open Source MongoDB alternative";
package = lib.mkPackageOption pkgs "ferretdb" { };
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
options = {
FERRETDB_HANDLER = lib.mkOption {
type = lib.types.enum [
"sqlite"
"pg"
];
default = "sqlite";
description = "Backend handler";
};
FERRETDB_SQLITE_URL = lib.mkOption {
type = lib.types.str;
default = "file:/var/lib/ferretdb/";
description = "SQLite URI (directory) for 'sqlite' handler";
};
FERRETDB_POSTGRESQL_URL = lib.mkOption {
type = lib.types.str;
default = "postgres://ferretdb@localhost/ferretdb?host=/run/postgresql";
description = "PostgreSQL URL for 'pg' handler";
};
FERRETDB_TELEMETRY = lib.mkOption {
type = lib.types.enum [
"enable"
"disable"
];
default = "disable";
description = ''
Enable or disable basic telemetry.
See <https://docs.ferretdb.io/telemetry/> for more information.
'';
};
};
};
example = {
FERRETDB_LOG_LEVEL = "warn";
FERRETDB_MODE = "normal";
};
description = ''
Additional configuration for FerretDB, see
<https://docs.ferretdb.io/configuration/flags/>
for supported values.
'';
};
};
};
config = lib.mkIf cfg.enable {
services.ferretdb.settings = { };
systemd.services.ferretdb = {
description = "FerretDB";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = cfg.settings;
serviceConfig = {
Type = "simple";
StateDirectory = "ferretdb";
WorkingDirectory = "/var/lib/ferretdb";
ExecStart = "${cfg.package}/bin/ferretdb";
Restart = "on-failure";
ProtectHome = true;
ProtectSystem = "strict";
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
NoNewPrivileges = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
DynamicUser = true;
};
};
};
}

View File

@@ -0,0 +1,164 @@
{
config,
lib,
pkgs,
...
}:
# TODO: This may file may need additional review, eg which configurations 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 chosen
# 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
# https://www.firebirdsql.org/manual/qsg25-classic-or-super.html
let
cfg = config.services.firebird;
firebird = cfg.package;
dataDir = "${cfg.baseDir}/data";
systemDir = "${cfg.baseDir}/system";
in
{
###### interface
options = {
services.firebird = {
enable = lib.mkEnableOption "the Firebird super server";
package = lib.mkPackageOption pkgs "firebird" {
example = "firebird_3";
extraDescription = ''
For SuperServer use override: `pkgs.firebird_3.override { superServer = true; };`
'';
};
port = lib.mkOption {
default = 3050;
type = lib.types.port;
description = ''
Port Firebird uses.
'';
};
user = lib.mkOption {
default = "firebird";
type = lib.types.str;
description = ''
User account under which firebird runs.
'';
};
baseDir = lib.mkOption {
default = "/var/lib/firebird";
type = lib.types.str;
description = ''
Location containing data/ and system/ directories.
data/ stores the databases, system/ stores the password database security2.fdb.
'';
};
};
};
###### implementation
config = lib.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 = ${toString cfg.port}
# randomly choose port for server Event Notification
#RemoteAuxPort = 0
# rsetrict connections to a network card:
#RemoteBindAddress =
# there are some additional settings which should be reviewed
'';
users.users.firebird = {
description = "Firebird server user";
group = "firebird";
uid = config.ids.uids.firebird;
};
users.groups.firebird.gid = config.ids.gids.firebird;
};
}

View File

@@ -0,0 +1,311 @@
# FoundationDB {#module-services-foundationdb}
*Source:* {file}`modules/services/databases/foundationdb.nix`
*Upstream documentation:* <https://apple.github.io/foundationdb/>
*Maintainer:* Austin Seipp
*Available version(s):* 7.1.x
FoundationDB (or "FDB") is an open source, distributed, transactional
key-value store.
## Configuring and basic setup {#module-services-foundationdb-configuring}
To enable FoundationDB, add the following to your
{file}`configuration.nix`:
```nix
{
services.foundationdb.enable = true;
services.foundationdb.package = pkgs.foundationdb73; # FoundationDB 7.3.x
}
```
The {option}`services.foundationdb.package` 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.
After running {command}`nixos-rebuild`, you can verify whether
FoundationDB is running by executing {command}`fdbcli` (which is
added to {option}`environment.systemPackages`):
```ShellSession
$ sudo -u foundationdb fdbcli
Using cluster file `/etc/foundationdb/fdb.cluster'.
The database is available.
Welcome to the fdbcli. For help, type `help'.
fdb> 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
...
fdb>
```
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` shebang support to automatically supply the
necessary Python modules).
```ShellSession
a@link> cat fdb-status.py
#! /usr/bin/env nix-shell
#! nix-shell -i python -p python pythonPackages.foundationdb73
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()
a@link> chmod +x fdb-status.py
a@link> ./fdb-status.py
FoundationDB available: True
a@link>
```
FoundationDB is run under the {command}`foundationdb` user and group
by default, but this may be changed in the NixOS configuration. The systemd
unit {command}`foundationdb.service` controls the
{command}`fdbmonitor` process.
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`.)
Furthermore, only 1 server process and 1 backup agent are started in the
default configuration. See below for more on scaling to increase this.
FoundationDB stores all data for all server processes under
{file}`/var/lib/foundationdb`. You can override this using
{option}`services.foundationdb.dataDir`, e.g.
```nix
{ services.foundationdb.dataDir = "/data/fdb"; }
```
Similarly, logs are stored under {file}`/var/log/foundationdb`
by default, and there is a corresponding
{option}`services.foundationdb.logDir` as well.
## Scaling processes and backup agents {#module-services-foundationdb-scaling}
Scaling the number of server processes is quite easy; simply specify
{option}`services.foundationdb.serverProcesses` to be the number of
FoundationDB worker processes that should be started on the machine.
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.
A similar option exists in order to scale backup agent processes,
{option}`services.foundationdb.backupProcesses`. Backup agents are
not as performance/RAM sensitive, so feel free to experiment with the number
of available backup processes.
## Clustering {#module-services-foundationdb-clustering}
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.
FoundationDB organizes clusters using a set of
*coordinators*, 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`.
Coordinators are specified globally using the
{command}`/etc/foundationdb/fdb.cluster` file, which all servers and
client applications will use to find and join coordinators. Note that this
file *can not* 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.
When dealing with a cluster, there are two main things you want to do:
- Add a node to the cluster for storage/compute.
- Promote an ordinary worker to a coordinator.
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.
To add a machine to a FoundationDB cluster:
- Choose one of the servers to start as the initial coordinator.
- Copy the {command}`/etc/foundationdb/fdb.cluster` file from this
server to all the other servers. Restart FoundationDB on all of these
other servers, so they join the cluster.
- All of these servers are now connected and working together in the
cluster, under the chosen coordinator.
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` to change this and add new coordinators.
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
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
*every* node a coordinator automatically:
```ShellSession
fdbcli> configure double ssd
fdbcli> coordinators auto
```
This will transparently update all the servers within seconds, and
appropriately rewrite the {command}`fdb.cluster` file, as well as
informing all client processes to do the same.
## Client connectivity {#module-services-foundationdb-connectivity}
By default, all clients must use the current {command}`fdb.cluster`
file to access a given FoundationDB cluster. This file is located by default
in {command}`/etc/foundationdb/fdb.cluster` 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.
## Client authorization and TLS {#module-services-foundationdb-authorization}
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 *Acme Co* in the
*Research and Development* unit.
Configuring TLS with FoundationDB is done using the
{option}`services.foundationdb.tls` options in order to control the
peer verification string, as well as the certificate and its private key.
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.
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` file to specify that a given set of
coordinators use TLS. This is as simple as adding the suffix
{command}`:tls` to your cluster coordinator configuration, after the
port number. For example, assuming you have a coordinator on localhost with
the default configuration, simply specifying:
```
XXXXXX:XXXXXX@127.0.0.1:4500:tls
```
will configure all clients and server processes to use TLS from now on.
## Backups and Disaster Recovery {#module-services-foundationdb-disaster-recovery}
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` systemd
unit uses *Linux namespaces* to restrict write access to
the system, except for the log directory, data directory, and the
{command}`/etc/foundationdb/` directory. This is enforced by default
and cannot be disabled.
However, a side effect of this is that the {command}`fdbbackup`
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.
In order to allow flexible backup locations on local disks, the FoundationDB
NixOS module supports a
{option}`services.foundationdb.extraReadWritePaths` 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.
For example, to create backups in {command}`/opt/fdb-backups`, first
set up the paths in the module options:
```nix
{ services.foundationdb.extraReadWritePaths = [ "/opt/fdb-backups" ]; }
```
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
*must* 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).
You can now perform a backup:
```ShellSession
$ sudo -u foundationdb fdbbackup start -t default -d file:///opt/fdb-backups
$ sudo -u foundationdb fdbbackup status -t default
```
## Known limitations {#module-services-foundationdb-limitations}
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.
- There is no way to specify individual parameters for individual
{command}`fdbserver` processes. Currently, all server processes
inherit all the global {command}`fdbmonitor` settings.
- Ruby bindings are not currently installed.
- Go bindings are not currently installed.
## Options {#module-services-foundationdb-options}
NixOS's FoundationDB module allows you to configure all of the most relevant
configuration options for {command}`fdbmonitor`, matching it quite
closely. A complete list of options for the FoundationDB module may be found
[here](#opt-services.foundationdb.enable). You should
also read the FoundationDB documentation as well.
## Full documentation {#module-services-foundationdb-full-docs}
FoundationDB is a complex piece of software, and requires careful
administration to properly use. Full documentation for administration can be
found here: <https://apple.github.io/foundationdb/>.

View File

@@ -0,0 +1,464 @@
{
config,
lib,
pkgs,
...
}:
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:
lib.concatStringsSep "\n" (
map (x: "[fdbserver.${toString (x + cfg.listenPortStart)}]") (lib.range 0 (n - 1))
);
backupAgents =
n: lib.concatStringsSep "\n" (map (x: "[backup_agent.${toString x}]") (lib.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}
${lib.optionalString (cfg.class != null) "class = ${cfg.class}"}
memory = ${cfg.memory}
storage_memory = ${cfg.storageMemory}
${lib.optionalString (lib.versionAtLeast cfg.package.version "6.1") ''
trace_format = ${cfg.traceFormat}
''}
${lib.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}
''}
${lib.optionalString (
cfg.locality.machineId != null
) "locality_machineid=${cfg.locality.machineId}"}
${lib.optionalString (cfg.locality.zoneId != null) "locality_zoneid=${cfg.locality.zoneId}"}
${lib.optionalString (
cfg.locality.datacenterId != null
) "locality_dcid=${cfg.locality.datacenterId}"}
${lib.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 = lib.mkEnableOption "FoundationDB Server";
package = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.str;
default = "auto";
description = "Publicly visible IP address of the process. Port is determined by process ID";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "public";
description = "Publicly visible IP address of the process. Port is determined by process ID";
};
listenPortStart = lib.mkOption {
type = lib.types.port;
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 = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Open the firewall ports corresponding to FoundationDB processes and coordinators
using {option}`config.networking.firewall.*`.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/foundationdb";
description = "Data directory. All cluster data will be put under here.";
};
logDir = lib.mkOption {
type = lib.types.path;
default = "/var/log/foundationdb";
description = "Log directory.";
};
user = lib.mkOption {
type = lib.types.str;
default = "foundationdb";
description = "User account under which FoundationDB runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "foundationdb";
description = "Group account under which FoundationDB runs.";
};
class = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"storage"
"transaction"
"stateless"
]
);
default = null;
description = "Process class";
};
restartDelay = lib.mkOption {
type = lib.types.int;
default = 10;
description = "Number of seconds to wait before restarting servers.";
};
logSize = lib.mkOption {
type = lib.types.str;
default = "10MiB";
description = ''
Roll over to a new log file after the current log file
reaches the specified size.
'';
};
maxLogSize = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Number of fdbserver processes to run.";
};
backupProcesses = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Number of backup_agent processes to run for snapshots.";
};
memory = lib.mkOption {
type = lib.types.str;
default = "8GiB";
description = ''
Maximum memory used by the process. The default value is
`8GiB`. When specified without a unit,
`MiB` 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 `8GiB`
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 `storageMemory`
parameter is increased, the `memory`
parameter should be increased by an equal amount.
'';
};
storageMemory = lib.mkOption {
type = lib.types.str;
default = "1GiB";
description = ''
Maximum memory used for data storage. The default value is
`1GiB`. When specified without a unit,
`MB` 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
`storageMemory`, you should also increase
the `memory` parameter by the same amount.
'';
};
tls = lib.mkOption {
default = null;
description = ''
FoundationDB Transport Security Layer (TLS) settings.
'';
type = lib.types.nullOr (
lib.types.submodule {
options = {
certificate = lib.mkOption {
type = lib.types.str;
description = ''
Path to the TLS certificate file. This certificate will
be offered to, and may be verified by, clients.
'';
};
key = lib.mkOption {
type = lib.types.str;
description = "Private key file for the certificate.";
};
allowedPeers = lib.mkOption {
type = lib.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 = lib.mkOption {
default = {
machineId = null;
zoneId = null;
datacenterId = null;
dataHall = null;
};
description = ''
FoundationDB locality settings.
'';
type = lib.types.submodule {
options = {
machineId = lib.mkOption {
default = null;
type = lib.types.nullOr lib.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 = lib.mkOption {
default = null;
type = lib.types.nullOr lib.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 = lib.mkOption {
default = null;
type = lib.types.nullOr lib.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 = lib.mkOption {
default = null;
type = lib.types.nullOr lib.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 = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.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 = lib.mkOption {
type = lib.types.path;
default = "/run/foundationdb.pid";
description = "Path to pidfile for fdbmonitor.";
};
traceFormat = lib.mkOption {
type = lib.types.enum [
"xml"
"json"
];
default = "xml";
description = "Trace logging format.";
};
};
config = lib.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 = lib.optionalAttrs (cfg.user == "foundationdb") {
foundationdb = {
description = "FoundationDB User";
uid = config.ids.uids.foundationdb;
group = cfg.group;
};
};
users.groups = lib.optionalAttrs (cfg.group == "foundationdb") {
foundationdb.gid = config.ids.gids.foundationdb;
};
networking.firewall.allowedTCPPortRanges = lib.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.md;
meta.maintainers = with lib.maintainers; [ thoughtpolice ];
}

View File

@@ -0,0 +1,150 @@
{
config,
options,
lib,
pkgs,
...
}:
let
cfg = config.services.hbase-standalone;
opt = options.services.hbase-standalone;
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
{
imports = [
(lib.mkRenamedOptionModule [ "services" "hbase" ] [ "services" "hbase-standalone" ])
];
###### interface
options = {
services.hbase-standalone = {
enable = lib.mkEnableOption ''
HBase master in standalone mode with embedded regionserver and zookeper.
Do not use this configuration for production nor for evaluating HBase performance
'';
package = lib.mkPackageOption pkgs "hbase" { };
user = lib.mkOption {
type = lib.types.str;
default = "hbase";
description = ''
User account under which HBase runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "hbase";
description = ''
Group account under which HBase runs.
'';
};
dataDir = lib.mkOption {
type = lib.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 = lib.mkOption {
type = lib.types.path;
default = "/var/log/hbase";
description = ''
Specifies the location of HBase log files.
'';
};
settings = lib.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 = lib.literalExpression ''
{
"hbase.rootdir" = "file://''${config.${opt.dataDir}}/hbase";
"hbase.zookeeper.property.dataDir" = "''${config.${opt.dataDir}}/zookeeper";
}
'';
description = ''
configurations in hbase-site.xml, see <https://github.com/apache/hbase/blob/master/hbase-server/src/test/resources/hbase-site.xml> for details.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
"d '${cfg.logDir}' - ${cfg.user} ${cfg.group} - -"
];
systemd.services.hbase = {
description = "HBase Server";
wantedBy = [ "multi-user.target" ];
environment = {
# JRE 15 removed option `UseConcMarkSweepGC` which is needed.
JAVA_HOME = "${pkgs.jre8}";
HBASE_LOG_DIR = cfg.logDir;
};
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/hbase --config ${configDir} master start";
};
};
users.users.hbase = {
description = "HBase Server user";
group = "hbase";
uid = config.ids.uids.hbase;
};
users.groups.hbase.gid = config.ids.gids.hbase;
};
}

View File

@@ -0,0 +1,198 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.influxdb;
configOptions = lib.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.formats.toml { }).generate "config.toml" configOptions;
in
{
###### interface
options = {
services.influxdb = {
enable = lib.mkOption {
default = false;
description = "Whether to enable the influxdb server";
type = lib.types.bool;
};
package = lib.mkPackageOption pkgs "influxdb" { };
user = lib.mkOption {
default = "influxdb";
description = "User account under which influxdb runs";
type = lib.types.str;
};
group = lib.mkOption {
default = "influxdb";
description = "Group under which influxdb runs";
type = lib.types.str;
};
dataDir = lib.mkOption {
default = "/var/db/influxdb";
description = "Data directory for influxd data files.";
type = lib.types.path;
};
extraConfig = lib.mkOption {
default = { };
description = "Extra configuration options for influxdb";
type = lib.types.attrs;
};
};
};
###### implementation
config = lib.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;
Restart = "on-failure";
};
postStart =
let
scheme = if configOptions.http.https-enabled then "-k https" else "http";
bindAddr = (ba: if lib.hasPrefix ":" ba then "127.0.0.1${ba}" else "${ba}") (
toString configOptions.http.bind-address
);
in
lib.mkBefore ''
until ${pkgs.curl.bin}/bin/curl -s -o /dev/null ${scheme}://${bindAddr}/ping; do
sleep 1;
done
'';
};
users.users = lib.optionalAttrs (cfg.user == "influxdb") {
influxdb = {
uid = config.ids.uids.influxdb;
group = "influxdb";
description = "Influxdb daemon user";
};
};
users.groups = lib.optionalAttrs (cfg.group == "influxdb") {
influxdb.gid = config.ids.gids.influxdb;
};
};
}

View File

@@ -0,0 +1,560 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
any
attrNames
attrValues
count
escapeShellArg
filterAttrs
flatten
flip
getExe
hasAttr
hasInfix
listToAttrs
literalExpression
mapAttrsToList
mkEnableOption
mkPackageOption
mkIf
mkOption
nameValuePair
optional
subtractLists
types
unique
;
format = pkgs.formats.json { };
cfg = config.services.influxdb2;
configFile = format.generate "config.json" cfg.settings;
validPermissions = [
"authorizations"
"buckets"
"dashboards"
"orgs"
"tasks"
"telegrafs"
"users"
"variables"
"secrets"
"labels"
"views"
"documents"
"notificationRules"
"notificationEndpoints"
"checks"
"dbrp"
"annotations"
"sources"
"scrapers"
"notebooks"
"remotes"
"replications"
];
# Determines whether at least one active api token is defined
anyAuthDefined = flip any (attrValues cfg.provision.organizations) (
o: o.present && flip any (attrValues o.auths) (a: a.present && a.tokenFile != null)
);
provisionState = pkgs.writeText "provision_state.json" (
builtins.toJSON {
inherit (cfg.provision) organizations users;
}
);
influxHost = "http://${
escapeShellArg (
if
!hasAttr "http-bind-address" cfg.settings || hasInfix "0.0.0.0" cfg.settings.http-bind-address
then
"localhost:8086"
else
cfg.settings.http-bind-address
)
}";
waitUntilServiceIsReady = pkgs.writeShellScript "wait-until-service-is-ready" ''
set -euo pipefail
export INFLUX_HOST=${influxHost}
count=0
while ! influx ping &>/dev/null; do
if [ "$count" -eq 300 ]; then
echo "Tried for 30 seconds, giving up..."
exit 1
fi
if ! kill -0 "$MAINPID"; then
echo "Main server died, giving up..."
exit 1
fi
sleep 0.1
count=$((count++))
done
'';
provisioningScript = pkgs.writeShellScript "post-start-provision" ''
set -euo pipefail
export INFLUX_HOST=${influxHost}
# Do the initial database setup. Pass /dev/null as configs-path to
# avoid saving the token as the active config.
if test -e "$STATE_DIRECTORY/.first_startup"; then
influx setup \
--configs-path /dev/null \
--org ${escapeShellArg cfg.provision.initialSetup.organization} \
--bucket ${escapeShellArg cfg.provision.initialSetup.bucket} \
--username ${escapeShellArg cfg.provision.initialSetup.username} \
--password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \
--token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \
--retention ${toString cfg.provision.initialSetup.retention}s \
--force >/dev/null
rm -f "$STATE_DIRECTORY/.first_startup"
fi
provision_result=$(${getExe pkgs.influxdb2-provision} ${provisionState} "$INFLUX_HOST" "$(< "$CREDENTIALS_DIRECTORY/admin-token")")
if [[ "$(jq '[.auths[] | select(.action == "created")] | length' <<< "$provision_result")" -gt 0 ]]; then
echo "Created at least one new token, queueing service restart so we can manipulate secrets"
touch "$STATE_DIRECTORY/.needs_restart"
fi
'';
restarterScript = pkgs.writeShellScript "post-start-restarter" ''
set -euo pipefail
if test -e "$STATE_DIRECTORY/.needs_restart"; then
rm -f "$STATE_DIRECTORY/.needs_restart"
/run/current-system/systemd/bin/systemctl restart influxdb2
fi
'';
organizationSubmodule = types.submodule (
organizationSubmod:
let
org = organizationSubmod.config._module.args.name;
in
{
options = {
present = mkOption {
description = "Whether to ensure that this organization is present or absent.";
type = types.bool;
default = true;
};
description = mkOption {
description = "Optional description for the organization.";
default = null;
type = types.nullOr types.str;
};
buckets = mkOption {
description = "Buckets to provision in this organization.";
default = { };
type = types.attrsOf (
types.submodule (
bucketSubmod:
let
bucket = bucketSubmod.config._module.args.name;
in
{
options = {
present = mkOption {
description = "Whether to ensure that this bucket is present or absent.";
type = types.bool;
default = true;
};
description = mkOption {
description = "Optional description for the bucket.";
default = null;
type = types.nullOr types.str;
};
retention = mkOption {
type = types.ints.unsigned;
default = 0;
description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
};
};
}
)
);
};
auths = mkOption {
description = "API tokens to provision for the user in this organization.";
default = { };
type = types.attrsOf (
types.submodule (
authSubmod:
let
auth = authSubmod.config._module.args.name;
in
{
options = {
id = mkOption {
description = "A unique identifier for this authentication token. Since influx doesn't store names for tokens, this will be hashed and appended to the description to identify the token.";
readOnly = true;
default = builtins.substring 0 32 (builtins.hashString "sha256" "${org}:${auth}");
defaultText = "<a hash derived from org and name>";
type = types.str;
};
present = mkOption {
description = "Whether to ensure that this user is present or absent.";
type = types.bool;
default = true;
};
description = mkOption {
description = ''
Optional description for the API token.
Note that the actual token will always be created with a descriptionregardless
of whether this is given or not. The name is always added plus a unique suffix
to later identify the token to track whether it has already been created.
'';
default = null;
type = types.nullOr types.str;
};
tokenFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "The token value. If not given, influx will automatically generate one.";
};
operator = mkOption {
description = "Grants all permissions in all organizations.";
default = false;
type = types.bool;
};
allAccess = mkOption {
description = "Grants all permissions in the associated organization.";
default = false;
type = types.bool;
};
readPermissions = mkOption {
description = ''
The read permissions to include for this token. Access is usually granted only
for resources in the associated organization.
Available permissions are `authorizations`, `buckets`, `dashboards`,
`orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
`documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
`annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
Refer to `influx auth create --help` for a full list with descriptions.
`buckets` grants read access to all associated buckets. Use `readBuckets` to define
more granular access permissions.
'';
default = [ ];
type = types.listOf (types.enum validPermissions);
};
writePermissions = mkOption {
description = ''
The read permissions to include for this token. Access is usually granted only
for resources in the associated organization.
Available permissions are `authorizations`, `buckets`, `dashboards`,
`orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
`documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
`annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
Refer to `influx auth create --help` for a full list with descriptions.
`buckets` grants write access to all associated buckets. Use `writeBuckets` to define
more granular access permissions.
'';
default = [ ];
type = types.listOf (types.enum validPermissions);
};
readBuckets = mkOption {
description = "The organization's buckets which should be allowed to be read";
default = [ ];
type = types.listOf types.str;
};
writeBuckets = mkOption {
description = "The organization's buckets which should be allowed to be written";
default = [ ];
type = types.listOf types.str;
};
};
}
)
);
};
};
}
);
in
{
options = {
services.influxdb2 = {
enable = mkEnableOption "the influxdb2 server";
package = mkPackageOption pkgs "influxdb2" { };
settings = mkOption {
default = { };
description = ''configuration options for influxdb2, see <https://docs.influxdata.com/influxdb/v2.0/reference/config-options> for details.'';
type = format.type;
};
provision = {
enable = mkEnableOption "initial database setup and provisioning";
initialSetup = {
organization = mkOption {
type = types.str;
example = "main";
description = "Primary organization name";
};
bucket = mkOption {
type = types.str;
example = "example";
description = "Primary bucket name";
};
username = mkOption {
type = types.str;
default = "admin";
description = "Primary username";
};
retention = mkOption {
type = types.ints.unsigned;
default = 0;
description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
};
passwordFile = mkOption {
type = types.path;
description = "Password for primary user. Don't use a file from the nix store!";
};
tokenFile = mkOption {
type = types.path;
description = "API Token to set for the admin user. Don't use a file from the nix store!";
};
};
organizations = mkOption {
description = "Organizations to provision.";
example = literalExpression ''
{
myorg = {
description = "My organization";
buckets.mybucket = {
description = "My bucket";
retention = 31536000; # 1 year
};
auths.mytoken = {
readBuckets = ["mybucket"];
tokenFile = "/run/secrets/mytoken";
};
};
}
'';
default = { };
type = types.attrsOf organizationSubmodule;
};
users = mkOption {
description = "Users to provision.";
default = { };
example = literalExpression ''
{
# admin = {}; /* The initialSetup.username will automatically be added. */
myuser.passwordFile = "/run/secrets/myuser_password";
}
'';
type = types.attrsOf (
types.submodule (
userSubmod:
let
user = userSubmod.config._module.args.name;
org = userSubmod.config.org;
in
{
options = {
present = mkOption {
description = "Whether to ensure that this user is present or absent.";
type = types.bool;
default = true;
};
passwordFile = mkOption {
description = "Password for the user. If unset, the user will not be able to log in until a password is set by an operator! Don't use a file from the nix store!";
default = null;
type = types.nullOr types.path;
};
};
}
)
);
};
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings);
message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
}
]
++ flatten (
flip mapAttrsToList cfg.provision.organizations (
orgName: org:
flip mapAttrsToList org.auths (
authName: auth: [
{
assertion =
1 == count (x: x) [
auth.operator
auth.allAccess
(
auth.readPermissions != [ ]
|| auth.writePermissions != [ ]
|| auth.readBuckets != [ ]
|| auth.writeBuckets != [ ]
)
];
message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings.";
}
(
let
unknownBuckets = subtractLists (attrNames org.buckets) auth.readBuckets;
in
{
assertion = unknownBuckets == [ ];
message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in readBuckets: ${toString unknownBuckets}";
}
)
(
let
unknownBuckets = subtractLists (attrNames org.buckets) auth.writeBuckets;
in
{
assertion = unknownBuckets == [ ];
message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in writeBuckets: ${toString unknownBuckets}";
}
)
]
)
)
);
services.influxdb2.provision = mkIf cfg.provision.enable {
organizations.${cfg.provision.initialSetup.organization} = {
buckets.${cfg.provision.initialSetup.bucket} = {
inherit (cfg.provision.initialSetup) retention;
};
};
users.${cfg.provision.initialSetup.username} = {
inherit (cfg.provision.initialSetup) passwordFile;
};
};
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;
ZONEINFO = "${pkgs.tzdata}/share/zoneinfo";
};
serviceConfig = {
Type = "exec"; # When credentials are used with systemd before v257 this is necessary to make the service start reliably (see systemd/systemd#33953)
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";
LoadCredential = mkIf cfg.provision.enable [
"admin-password:${cfg.provision.initialSetup.passwordFile}"
"admin-token:${cfg.provision.initialSetup.tokenFile}"
];
ExecStartPost = [
waitUntilServiceIsReady
]
++ (lib.optionals cfg.provision.enable (
[ provisioningScript ]
++
# Only the restarter runs with elevated privileges
optional anyAuthDefined "+${restarterScript}"
));
};
path = [
pkgs.influxdb2-cli
pkgs.jq
];
# Mark if this is the first startup so postStart can do the initial setup.
# Also extract any token secret mappings and apply them if this isn't the first start.
preStart =
let
tokenPaths = listToAttrs (
flatten
# For all organizations
(
flip mapAttrsToList cfg.provision.organizations
# For each contained token that has a token file
(
_: org:
flip mapAttrsToList (filterAttrs (_: x: x.tokenFile != null) org.auths)
# Collect id -> tokenFile for the mapping
(_: auth: nameValuePair auth.id auth.tokenFile)
)
)
);
tokenMappings = pkgs.writeText "token_mappings.json" (builtins.toJSON tokenPaths);
in
mkIf cfg.provision.enable ''
if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then
touch "$STATE_DIRECTORY/.first_startup"
else
# Manipulate provisioned api tokens if necessary
${getExe pkgs.influxdb2-token-manipulator} "$STATE_DIRECTORY/influxd.bolt" ${tokenMappings}
fi
'';
};
users.extraUsers.influxdb2 = {
isSystemUser = true;
group = "influxdb2";
};
users.extraGroups.influxdb2 = { };
};
meta.maintainers = with lib.maintainers; [
nickcao
oddlama
];
}

View File

@@ -0,0 +1,242 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.lldap;
format = pkgs.formats.toml { };
in
{
options.services.lldap = with lib; {
enable = mkEnableOption "lldap, a lightweight authentication server that provides an opinionated, simplified LDAP interface for authentication";
package = mkPackageOption pkgs "lldap" { };
environment = mkOption {
type = with types; attrsOf str;
default = { };
example = {
LLDAP_JWT_SECRET_FILE = "/run/lldap/jwt_secret";
LLDAP_LDAP_USER_PASS_FILE = "/run/lldap/user_password";
};
description = ''
Environment variables passed to the service.
Any config option name prefixed with `LLDAP_` takes priority over the one in the configuration file.
'';
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Environment file as defined in {manpage}`systemd.exec(5)` passed to the service.
'';
};
settings = mkOption {
description = ''
Free-form settings written directly to the `lldap_config.toml` file.
Refer to <https://github.com/lldap/lldap/blob/main/lldap_config.docker_template.toml> for supported values.
'';
default = { };
type = types.submodule {
freeformType = format.type;
options = {
ldap_host = mkOption {
type = types.str;
description = "The host address that the LDAP server will be bound to.";
default = "::";
};
ldap_port = mkOption {
type = types.port;
description = "The port on which to have the LDAP server.";
default = 3890;
};
http_host = mkOption {
type = types.str;
description = "The host address that the HTTP server will be bound to.";
default = "::";
};
http_port = mkOption {
type = types.port;
description = "The port on which to have the HTTP server, for user login and administration.";
default = 17170;
};
http_url = mkOption {
type = types.str;
description = "The public URL of the server, for password reset links.";
default = "http://localhost";
};
ldap_base_dn = mkOption {
type = types.str;
description = "Base DN for LDAP.";
example = "dc=example,dc=com";
};
ldap_user_dn = mkOption {
type = types.str;
description = "Admin username";
default = "admin";
};
ldap_user_email = mkOption {
type = types.str;
description = "Admin email.";
default = "admin@example.com";
};
database_url = mkOption {
type = types.str;
description = "Database URL.";
default = "sqlite://./users.db?mode=rwc";
example = "postgres://postgres-user:password@postgres-server/my-database";
};
ldap_user_pass = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Password for default admin password.
Unsecure: Use `ldap_user_pass_file` settings instead.
'';
};
ldap_user_pass_file = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Path to a file containing the default admin password.
If you want to update the default admin password through this setting,
you must set `force_ldap_user_pass_reset` to `true`.
Otherwise changing this setting will have no effect
unless this is the very first time LLDAP is started and its database is still empty.
'';
};
force_ldap_user_pass_reset = mkOption {
type = types.oneOf [
types.bool
(types.enum [ "always" ])
];
default = false;
description = ''
Force reset of the admin password.
Set this setting to `"always"` to update the admin password when `ldap_user_pass_file` changes.
Setting to `"always"` also means any password update in the UI will be overwritten next time the service restarts.
The difference between `true` and `"always"` is the former is intended for a one time fix
while the latter is intended for a declarative workflow. In practice, the result
is the same: the password gets reset. The only practical difference is the former
outputs a warning message while the latter outputs an info message.
'';
};
jwt_secret_file = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Path to a file containing the JWT secret.
'';
};
};
};
# TOML does not allow null values, so we use null to omit those fields
apply = lib.filterAttrsRecursive (_: v: v != null);
};
silenceForceUserPassResetWarning = mkOption {
type = types.bool;
default = false;
description = ''
Disable warning when the admin password is set declaratively with the `ldap_user_pass_file` setting
but the `force_ldap_user_pass_reset` is set to `false`.
This can lead to the admin password to drift from the one given declaratively.
If that is okay for you and you want to silence the warning, set this option to `true`.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.settings.ldap_user_pass_file or null) != null
|| (cfg.settings.ldap_user_pass or null) != null
|| (cfg.environment.LLDAP_LDAP_USER_PASS_FILE or null) != null;
message = "lldap: Default admin user password must be set. Please set the `ldap_user_pass` or better the `ldap_user_pass_file` setting. Alternatively, you can set the `LLDAP_LDAP_USER_PASS_FILE` environment variable.";
}
{
assertion =
(cfg.settings.ldap_user_pass_file or null) == null || (cfg.settings.ldap_user_pass or null) == null;
message = "lldap: Both `ldap_user_pass` and `ldap_user_pass_file` settings should not be set at the same time. Set one to `null`.";
}
];
warnings =
lib.optionals (cfg.settings.ldap_user_pass or null != null) [
''
lldap: Unsecure `ldap_user_pass` setting is used. Prefer `ldap_user_pass_file` instead.
''
]
++
lib.optionals
(cfg.settings.force_ldap_user_pass_reset == false && cfg.silenceForceUserPassResetWarning == false)
[
''
lldap: The `force_ldap_user_pass_reset` setting is set to `false` which means
the admin password can be changed through the UI and will drift from the one defined in your nix config.
It also means changing the setting `ldap_user_pass` or `ldap_user_pass_file` will have no effect on the admin password.
Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`.
''
];
systemd.services.lldap = {
description = "Lightweight LDAP server (lldap)";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
# lldap defaults to a hardcoded `jwt_secret` value if none is provided, which is bad, because
# an attacker could create a valid admin jwt access token fairly trivially.
# Because there are 3 different ways `jwt_secret` can be provided, we check if any one of them is present,
# and if not, bootstrap a secret in `/var/lib/lldap/jwt_secret_file` and give that to lldap.
script =
lib.optionalString (!cfg.settings ? jwt_secret) ''
if [[ -z "$LLDAP_JWT_SECRET_FILE" ]] && [[ -z "$LLDAP_JWT_SECRET" ]]; then
if [[ ! -e "./jwt_secret_file" ]]; then
${lib.getExe pkgs.openssl} rand -base64 -out ./jwt_secret_file 32
fi
export LLDAP_JWT_SECRET_FILE="./jwt_secret_file"
fi
''
+ ''
${lib.getExe cfg.package} run --config-file ${format.generate "lldap_config.toml" cfg.settings}
'';
serviceConfig = {
StateDirectory = "lldap";
StateDirectoryMode = "0750";
WorkingDirectory = "%S/lldap";
UMask = "0027";
User = "lldap";
Group = "lldap";
DynamicUser = true;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
};
inherit (cfg) environment;
};
};
}

View File

@@ -0,0 +1,123 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.memcached;
memcached = pkgs.memcached;
in
{
###### interface
options = {
services.memcached = {
enable = lib.mkEnableOption "Memcached";
user = lib.mkOption {
type = lib.types.str;
default = "memcached";
description = "The user to run Memcached as";
};
listen = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "The IP address to bind to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 11211;
description = "The port to bind to.";
};
enableUnixSocket = lib.mkEnableOption "Unix Domain Socket at /run/memcached/memcached.sock instead of listening on an IP address and port. The `listen` and `port` options are ignored";
maxMemory = lib.mkOption {
type = lib.types.ints.unsigned;
default = 64;
description = "The maximum amount of memory to use for storage, in MiB (1024×1024 bytes).";
};
maxConnections = lib.mkOption {
type = lib.types.ints.unsigned;
default = 1024;
description = "The maximum number of simultaneous connections.";
};
extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "A list of extra options that will be added as a suffix when running memcached.";
};
};
};
###### implementation
config = lib.mkIf config.services.memcached.enable {
users.users = lib.optionalAttrs (cfg.user == "memcached") {
memcached.description = "Memcached server user";
memcached.isSystemUser = true;
memcached.group = "memcached";
};
users.groups = lib.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} ${lib.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 = [
(lib.mkRemovedOptionModule [ "services" "memcached" "socket" ] ''
This option was replaced by a fixed unix socket path at /run/memcached/memcached.sock enabled using services.memcached.enableUnixSocket.
'')
];
}

View File

@@ -0,0 +1,98 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.monetdb;
in
{
meta.maintainers = with lib.maintainers; [ StillerHarpo ];
###### interface
options = {
services.monetdb = {
enable = lib.mkEnableOption "the MonetDB database server";
package = lib.mkPackageOption pkgs "monetdb" { };
user = lib.mkOption {
type = lib.types.str;
default = "monetdb";
description = "User account under which MonetDB runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "monetdb";
description = "Group under which MonetDB runs.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/monetdb";
description = "Data directory for the dbfarm.";
};
port = lib.mkOption {
type = lib.types.port;
default = 50000;
description = "Port to listen on.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = "Address to listen on.";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.users.monetdb = lib.mkIf (cfg.user == "monetdb") {
uid = config.ids.uids.monetdb;
group = cfg.group;
description = "MonetDB user";
home = cfg.dataDir;
createHome = true;
};
users.groups.monetdb = lib.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}
'';
};
};
}

View File

@@ -0,0 +1,212 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mongodb;
mongodb = cfg.package;
mongoshExe = lib.getExe cfg.mongoshPackage;
mongoCnf =
cfg:
pkgs.writeText "mongodb.conf" ''
net.bindIp: ${cfg.bind_ip}
${lib.optionalString cfg.quiet "systemLog.quiet: true"}
systemLog.destination: syslog
storage.dbPath: ${cfg.dbpath}
${lib.optionalString cfg.enableAuth "security.authorization: enabled"}
${lib.optionalString (cfg.replSetName != "") "replication.replSetName: ${cfg.replSetName}"}
${cfg.extraConfig}
'';
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"mongodb"
"initialRootPassword"
] "Use services.mongodb.initialRootPasswordFile to securely provide the initial root password.")
];
###### interface
options = {
services.mongodb = {
enable = lib.mkEnableOption "the MongoDB server";
package = lib.mkPackageOption pkgs "mongodb" {
example = "pkgs.mongodb-ce";
};
mongoshPackage = lib.mkPackageOption pkgs "mongosh" { };
user = lib.mkOption {
type = lib.types.str;
default = "mongodb";
description = "User account under which MongoDB runs";
};
bind_ip = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "IP to bind to";
};
quiet = lib.mkOption {
type = lib.types.bool;
default = false;
description = "quieter output";
};
enableAuth = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable client authentication. Creates a default superuser with username root!";
};
initialRootPasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to the file containing the password for the root user if auth is enabled.";
};
dbpath = lib.mkOption {
type = lib.types.str;
default = "/var/db/mongodb";
description = "Location where MongoDB stores its files";
};
pidFile = lib.mkOption {
type = lib.types.str;
default = "/run/mongodb.pid";
description = "Location of MongoDB pid file";
};
replSetName = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
If this instance is part of a replica set, set its name here.
Otherwise, leave empty to run as single node.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
storage.journal.enabled: false
'';
description = "MongoDB extra configuration in YAML format";
};
initialScript = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
A file containing MongoDB statements to execute on first startup.
'';
};
};
};
###### implementation
config = lib.mkIf config.services.mongodb.enable {
assertions = [
{
assertion = !cfg.enableAuth || cfg.initialRootPasswordFile != null;
message = "`enableAuth` requires `initialRootPasswordFile` to be set.";
}
];
users.users.mongodb = lib.mkIf (cfg.user == "mongodb") {
name = "mongodb";
isSystemUser = true;
group = "mongodb";
description = "MongoDB server user";
};
users.groups.mongodb = lib.mkIf (cfg.user == "mongodb") { };
systemd.services.mongodb = {
description = "MongoDB server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${mongodb}/bin/mongod --config ${mongoCnf cfg} --fork --pidfilepath ${cfg.pidFile}";
User = cfg.user;
PIDFile = cfg.pidFile;
Type = "forking";
TimeoutStartSec = 120; # initial creating of journal can take some time
PermissionsStartOnly = true;
};
preStart =
let
cfg_ = cfg // {
enableAuth = false;
bind_ip = "127.0.0.1";
};
in
''
rm ${cfg.dbpath}/mongod.lock || true
if ! test -e ${cfg.dbpath}; then
install -d -m0700 -o ${cfg.user} ${cfg.dbpath}
# See postStart!
touch ${cfg.dbpath}/.first_startup
fi
if ! test -e ${cfg.pidFile}; then
install -D -o ${cfg.user} /dev/null ${cfg.pidFile}
fi ''
+ lib.optionalString cfg.enableAuth ''
if ! test -e "${cfg.dbpath}/.auth_setup_complete"; then
systemd-run --unit=mongodb-for-setup --uid=${cfg.user} ${mongodb}/bin/mongod --config ${mongoCnf cfg_}
# wait for mongodb
while ! ${mongoshExe} --eval "db.version()" > /dev/null 2>&1; do sleep 0.1; done
initialRootPassword=$(<${cfg.initialRootPasswordFile})
${mongoshExe} <<EOF
use admin;
db.createUser(
{
user: "root",
pwd: "$initialRootPassword",
roles: [
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "dbAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" }
]
}
)
EOF
touch "${cfg.dbpath}/.auth_setup_complete"
systemctl stop mongodb-for-setup
fi
'';
postStart = ''
if test -e "${cfg.dbpath}/.first_startup"; then
${lib.optionalString (cfg.initialScript != null) ''
${lib.optionalString (cfg.enableAuth) "initialRootPassword=$(<${cfg.initialRootPasswordFile})"}
${mongoshExe} ${lib.optionalString (cfg.enableAuth) "-u root -p $initialRootPassword"} admin "${cfg.initialScript}"
''}
rm -f "${cfg.dbpath}/.first_startup"
fi
'';
};
};
}

View File

@@ -0,0 +1,751 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mysql;
isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
isOracle = lib.getName cfg.package == lib.getName pkgs.mysql80;
# Oracle MySQL has supported "notify" service type since 8.0
hasNotify = isMariaDB || (isOracle && lib.versionAtLeast cfg.package.version "8.0");
mysqldOptions = "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
configFile = format.generate "my.cnf" cfg.settings;
generateClusterAddressExpr = ''
if (config.services.mysql.galeraCluster.nodeAddresses == [ ]) then
""
else
"gcomm://''${builtins.concatStringsSep \",\" config.services.mysql.galeraCluster.nodeAddresses}"
+ lib.optionalString (config.services.mysql.galeraCluster.clusterPassword != "")
"?gmcast.seg=1:''${config.services.mysql.galeraCluster.clusterPassword}"
'';
generateClusterAddress =
if (cfg.galeraCluster.nodeAddresses == [ ]) then
""
else
"gcomm://${builtins.concatStringsSep "," cfg.galeraCluster.nodeAddresses}"
+ lib.optionalString (
cfg.galeraCluster.clusterPassword != ""
) "?gmcast.seg=1:${cfg.galeraCluster.clusterPassword}";
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"mysql"
"pidDir"
] "Don't wait for pidfiles, describe dependencies through systemd.")
(lib.mkRemovedOptionModule [
"services"
"mysql"
"rootPassword"
] "Use socket authentication or set the password outside of the nix store.")
(lib.mkRemovedOptionModule [
"services"
"mysql"
"extraOptions"
] "Use services.mysql.settings.mysqld instead.")
(lib.mkRemovedOptionModule [
"services"
"mysql"
"bind"
] "Use services.mysql.settings.mysqld.bind-address instead.")
(lib.mkRemovedOptionModule [
"services"
"mysql"
"port"
] "Use services.mysql.settings.mysqld.port instead.")
];
###### interface
options = {
services.mysql = {
enable = lib.mkEnableOption "MySQL server";
package = lib.mkOption {
type = lib.types.package;
example = lib.literalExpression "pkgs.mariadb";
description = ''
Which MySQL derivation to use. MariaDB packages are supported too.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "mysql";
description = ''
User account under which MySQL runs.
::: {.note}
If left as the default value this user will automatically be created
on system activation, otherwise you are responsible for
ensuring the user exists before the MySQL service starts.
:::
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "mysql";
description = ''
Group account under which MySQL runs.
::: {.note}
If left as the default value this group will automatically be created
on system activation, otherwise you are responsible for
ensuring the user exists before the MySQL service starts.
:::
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
example = "/var/lib/mysql";
description = ''
The data directory for MySQL.
::: {.note}
If left as the default value of `/var/lib/mysql` this directory will automatically be created before the MySQL
server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
:::
'';
};
configFile = lib.mkOption {
type = lib.types.path;
default = configFile;
defaultText = ''
A configuration file automatically generated by NixOS.
'';
description = ''
Override the configuration file used by MySQL. By default,
NixOS generates one automatically from {option}`services.mysql.settings`.
'';
example = lib.literalExpression ''
pkgs.writeText "my.cnf" '''
[mysqld]
datadir = /var/lib/mysql
bind-address = 127.0.0.1
port = 3336
!includedir /etc/mysql/conf.d/
''';
'';
};
settings = lib.mkOption {
type = format.type;
default = { };
description = ''
MySQL configuration. Refer to
<https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html>,
<https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html>,
and <https://mariadb.com/kb/en/server-system-variables/>
for details on supported values.
::: {.note}
MySQL configuration options such as `--quick` should be treated as
boolean options and provided values such as `true`, `false`,
`1`, or `0`. See the provided example below.
:::
'';
example = lib.literalExpression ''
{
mysqld = {
key_buffer_size = "6G";
table_cache = 1600;
log-error = "/var/log/mysql_err.log";
plugin-load-add = [ "server_audit" "ed25519=auth_ed25519" ];
};
mysqldump = {
quick = true;
max_allowed_packet = "16M";
};
}
'';
};
initialDatabases = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = ''
The name of the database to create.
'';
};
schema = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
The initial schema of the database; if null (the default),
an empty database is created.
'';
};
};
}
);
default = [ ];
description = ''
List of database names and their initial schemas that should be used to create databases on the first startup
of MySQL. The schema attribute is optional: If not specified, an empty database is created.
'';
example = lib.literalExpression ''
[
{ name = "foodatabase"; schema = ./foodatabase.sql; }
{ name = "bardatabase"; }
]
'';
};
initialScript = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database.";
};
ensureDatabases = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Ensures that the specified databases exist.
This option will never delete existing databases, especially not when the value of this
option is changed. This means that databases created once through this option or
otherwise have to be removed manually.
'';
example = [
"nextcloud"
"matomo"
];
};
ensureUsers = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = ''
Name of the user to ensure.
'';
};
ensurePermissions = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = ''
Permissions to ensure for the user, specified as attribute set.
The attribute names specify the database and tables to grant the permissions for,
separated by a dot. You may use wildcards here.
The attribute values specfiy the permissions to grant.
You may specify one or multiple comma-separated SQL privileges here.
For more information on how to specify the target
and on which privileges exist, see the
[GRANT syntax](https://mariadb.com/kb/en/library/grant/).
The attributes are used as `GRANT ''${attrName} ON ''${attrValue}`.
'';
example = lib.literalExpression ''
{
"database.*" = "ALL PRIVILEGES";
"*.*" = "SELECT, LOCK TABLES";
}
'';
};
};
}
);
default = [ ];
description = ''
Ensures that the specified users exist and have at least the ensured permissions.
The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the
same name only, and that without the need for a password.
This option will never delete existing users or remove permissions, especially not when the value of this
option is changed. This means that users created and permissions assigned once through this option or
otherwise have to be removed manually.
'';
example = lib.literalExpression ''
[
{
name = "nextcloud";
ensurePermissions = {
"nextcloud.*" = "ALL PRIVILEGES";
};
}
{
name = "backup";
ensurePermissions = {
"*.*" = "SELECT, LOCK TABLES";
};
}
]
'';
};
replication = {
role = lib.mkOption {
type = lib.types.enum [
"master"
"slave"
"none"
];
default = "none";
description = "Role of the MySQL server instance.";
};
serverId = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Id of the MySQL server instance. This number must be unique for each instance.";
};
masterHost = lib.mkOption {
type = lib.types.str;
description = "Hostname of the MySQL master server.";
};
slaveHost = lib.mkOption {
type = lib.types.str;
description = "Hostname of the MySQL slave server.";
};
masterUser = lib.mkOption {
type = lib.types.str;
description = "Username of the MySQL replication user.";
};
masterPassword = lib.mkOption {
type = lib.types.str;
description = "Password of the MySQL replication user.";
};
masterPort = lib.mkOption {
type = lib.types.port;
default = 3306;
description = "Port number on which the MySQL master server runs.";
};
};
galeraCluster = {
enable = lib.mkEnableOption "MariaDB Galera Cluster";
package = lib.mkOption {
type = lib.types.package;
description = "The MariaDB Galera package that provides the shared library 'libgalera_smm.so' required for cluster functionality.";
default = lib.literalExpression "pkgs.mariadb-galera";
};
name = lib.mkOption {
type = lib.types.str;
description = "The logical name of the Galera cluster. All nodes in the same cluster must use the same name.";
default = "galera";
};
sstMethod = lib.mkOption {
type = lib.types.enum [
"rsync"
"mariabackup"
];
description = "Method for the initial state transfer (wsrep_sst_method) when a node joins the cluster. Be aware that rsync needs SSH keys to be generated and authorized on all nodes!";
default = "rsync";
example = "mariabackup";
};
localName = lib.mkOption {
type = lib.types.str;
description = "The unique name that identifies this particular node within the cluster. Each node must have a different name.";
example = "node1";
};
localAddress = lib.mkOption {
type = lib.types.str;
description = "IP address or hostname of this node that will be used for cluster communication. Must be reachable by all other nodes.";
example = "1.2.3.4";
default = cfg.galeraCluster.localName;
defaultText = lib.literalExpression "config.services.mysql.galeraCluster.localName";
};
nodeAddresses = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "IP addresses or hostnames of all nodes in the cluster, including this node. This is used to construct the default clusterAddress connection string.";
example = lib.literalExpression ''["10.0.0.10" "10.0.0.20" "10.0.0.30"]'';
default = [ ];
};
clusterPassword = lib.mkOption {
type = lib.types.str;
description = "Optional password for securing cluster communications. If provided, it will be used in the clusterAddress for authentication between nodes.";
example = "SomePassword";
default = "";
};
clusterAddress = lib.mkOption {
type = lib.types.str;
description = "Full Galera cluster connection string. If nodeAddresses is set, this will be auto-generated, but you can override it with a custom value. Format is typically 'gcomm://node1,node2,node3' with optional parameters.";
example = "gcomm://10.0.0.10,10.0.0.20,10.0.0.30?gmcast.seg=1:SomePassword";
default = ""; # will be evaluate by generateClusterAddress
defaultText = lib.literalExpression generateClusterAddressExpr;
};
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !cfg.galeraCluster.enable || isMariaDB;
message = "'services.mysql.galeraCluster.enable' expect services.mysql.package to be an mariadb variant";
}
]
# galeraCluster options checks
++ lib.optionals cfg.galeraCluster.enable [
{
assertion =
cfg.galeraCluster.localAddress != ""
&& (cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "");
message = "mariadb galera cluster is enabled but the localAddress and (nodeAddresses or clusterAddress) are not set";
}
{
assertion = cfg.galeraCluster.clusterPassword == "" || cfg.galeraCluster.clusterAddress == "";
message = "mariadb galera clusterPassword is set but overwritten by clusterAddress";
}
{
assertion = cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "";
message = "When services.mysql.galeraCluster.clusterAddress is set, setting services.mysql.galeraCluster.nodeAddresses is redundant and will be overwritten by clusterAddress. Choose one approach.";
}
];
services.mysql.dataDir = lib.mkDefault (
if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql"
);
services.mysql.settings.mysqld = lib.mkMerge [
{
datadir = cfg.dataDir;
port = lib.mkDefault 3306;
}
(lib.mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
log-bin = "mysql-bin-${toString cfg.replication.serverId}";
log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
relay-log = "mysql-relay-bin";
server-id = cfg.replication.serverId;
binlog-ignore-db = [
"information_schema"
"performance_schema"
"mysql"
];
})
(lib.mkIf (!isMariaDB) {
plugin-load-add = [ "auth_socket.so" ];
})
(lib.mkIf cfg.galeraCluster.enable {
# Ensure Only InnoDB is used as galera clusters can only work with them
enforce_storage_engine = "InnoDB";
default_storage_engine = "InnoDB";
# galera only support this binlog format
binlog-format = "ROW";
bind_address = lib.mkDefault "0.0.0.0";
})
];
services.mysql.settings.galera = lib.optionalAttrs cfg.galeraCluster.enable {
wsrep_on = "ON";
wsrep_debug = lib.mkDefault "NONE";
wsrep_retry_autocommit = lib.mkDefault "3";
wsrep_provider = "${cfg.galeraCluster.package}/lib/galera/libgalera_smm.so";
wsrep_cluster_name = cfg.galeraCluster.name;
wsrep_cluster_address =
if (cfg.galeraCluster.clusterAddress != "") then
cfg.galeraCluster.clusterAddress
else
generateClusterAddress;
wsrep_node_address = cfg.galeraCluster.localAddress;
wsrep_node_name = "${cfg.galeraCluster.localName}";
# SST method using rsync
wsrep_sst_method = lib.mkDefault cfg.galeraCluster.sstMethod;
wsrep_sst_auth = lib.mkDefault "check_repl:check_pass";
binlog_format = "ROW";
innodb_autoinc_lock_mode = 2;
};
users.users = lib.optionalAttrs (cfg.user == "mysql") {
mysql = {
description = "MySQL server user";
group = cfg.group;
uid = config.ids.uids.mysql;
};
};
users.groups = lib.optionalAttrs (cfg.group == "mysql") {
mysql.gid = config.ids.gids.mysql;
};
environment.systemPackages = [ cfg.package ];
environment.etc."my.cnf".source = cfg.configFile;
# The mysql_install_db binary will try to adjust the permissions, but fail to do so with a permission
# denied error in some circumstances. Setting the permissions manually with tmpfiles is a workaround.
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} - -"
];
systemd.services.mysql = {
description = "MySQL Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ cfg.configFile ];
unitConfig.RequiresMountsFor = cfg.dataDir;
path = [
# Needed for the mysql_install_db command in the preStart script
# which calls the hostname command.
pkgs.hostname-debian
]
# tools 'wsrep_sst_rsync' needs
++ lib.optionals cfg.galeraCluster.enable [
cfg.package
pkgs.bash
pkgs.gawk
pkgs.gnutar
pkgs.gzip
pkgs.inetutils
pkgs.iproute2
pkgs.netcat
pkgs.procps
pkgs.pv
pkgs.rsync
pkgs.socat
pkgs.stunnel
pkgs.which
];
preStart =
if isMariaDB then
''
if ! test -e ${cfg.dataDir}/mysql; then
${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
touch ${cfg.dataDir}/mysql_init
fi
''
else
''
if ! test -e ${cfg.dataDir}/mysql; then
${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
touch ${cfg.dataDir}/mysql_init
fi
'';
script = ''
# https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
if test -n "''${_WSREP_START_POSITION}"; then
if test -e "${cfg.package}/bin/galera_recovery"; then
VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
fi
fi
# The last two environment variables are used for starting Galera clusters
exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
'';
postStart =
let
# The super user account to use on *first* run of MySQL server
superUser = if isMariaDB then cfg.user else "root";
in
''
${lib.optionalString (!hasNotify) ''
# Wait until the MySQL server is available for use
while [ ! -e /run/mysqld/mysqld.sock ]
do
echo "MySQL daemon not yet started. Waiting for 1 second..."
sleep 1
done
''}
${lib.optionalString isMariaDB ''
# If MariaDB is used in an Galera cluster, we have to check if the sync is done,
# or it will fail to init the database while joining, so we get in an broken non recoverable state
# so we wait until we have an synced state
if ${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW VARIABLES LIKE 'wsrep_on'" 2>/dev/null | ${lib.getExe' pkgs.gnugrep "grep"} -q 'ON'; then
echo "Galera cluster detected, waiting for node to be synced..."
while true; do
STATE=$(${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW STATUS LIKE 'wsrep_local_state_comment'" | ${lib.getExe' pkgs.gawk "awk"} '{print $2}')
if [ "$STATE" = "Synced" ]; then
echo "Node is synced"
break
else
echo "Current state: $STATE - Waiting for 1 second..."
sleep 1
fi
done
fi
''}
if [ -f ${cfg.dataDir}/mysql_init ]
then
# While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
# Since we don't want to run this service as 'root' we need to ensure the account exists on first run
( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${
if isMariaDB then "unix_socket" else "auth_socket"
};"
echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
) | ${cfg.package}/bin/mysql -u ${superUser} -N
${lib.concatMapStrings (database: ''
# Create initial databases
if ! test -e "${cfg.dataDir}/${database.name}"; then
echo "Creating initial database: ${database.name}"
( echo 'CREATE DATABASE IF NOT EXISTS `${database.name}`;'
${lib.optionalString (database.schema != null) ''
echo 'USE `${database.name}`;'
# TODO: this silently falls through if database.schema does not exist,
# we should catch this somehow and exit, but can't do it here because we're in a subshell.
if [ -f "${database.schema}" ]
then
cat ${database.schema}
elif [ -d "${database.schema}" ]
then
cat ${database.schema}/mysql-databases/*.sql
fi
''}
) | ${cfg.package}/bin/mysql -u ${superUser} -N
fi
'') cfg.initialDatabases}
${lib.optionalString (cfg.replication.role == "master") ''
# Set up the replication master
( echo "USE mysql;"
echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
) | ${cfg.package}/bin/mysql -u ${superUser} -N
''}
${lib.optionalString (cfg.replication.role == "slave") ''
# Set up the replication slave
( echo "STOP SLAVE;"
echo "CHANGE MASTER TO MASTER_HOST='${cfg.replication.masterHost}', MASTER_USER='${cfg.replication.masterUser}', MASTER_PASSWORD='${cfg.replication.masterPassword}';"
echo "START SLAVE;"
) | ${cfg.package}/bin/mysql -u ${superUser} -N
''}
${lib.optionalString (cfg.initialScript != null) ''
# Execute initial script
# using toString to avoid copying the file to nix store if given as path instead of string,
# as it might contain credentials
cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
''}
rm ${cfg.dataDir}/mysql_init
fi
${lib.optionalString (cfg.ensureDatabases != [ ]) ''
(
${lib.concatMapStrings (database: ''
echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
'') cfg.ensureDatabases}
) | ${cfg.package}/bin/mysql -N
''}
${lib.concatMapStrings (user: ''
( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${
if isMariaDB then "unix_socket" else "auth_socket"
};"
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (database: permission: ''
echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
'') user.ensurePermissions
)}
) | ${cfg.package}/bin/mysql -N
'') cfg.ensureUsers}
'';
serviceConfig = lib.mkMerge [
{
Type = if hasNotify then "notify" else "simple";
Restart = "on-abnormal";
RestartSec = "5s";
# User and group
User = cfg.user;
Group = cfg.group;
# Runtime directory and mode
RuntimeDirectory = "mysqld";
RuntimeDirectoryMode = "0755";
# Access write directories
ReadWritePaths = [ cfg.dataDir ];
# Capabilities
CapabilityBoundingSet = "";
# Security
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
# System Call Filtering
SystemCallArchitectures = "native";
}
(lib.mkIf (cfg.dataDir == "/var/lib/mysql") {
StateDirectory = "mysql";
StateDirectoryMode = "0700";
})
];
};
# Open firewall ports for MySQL (and Galera)
networking.firewall.allowedTCPPorts = lib.optionals cfg.galeraCluster.enable [
3306 # MySQL
4567 # Galera Cluster
4568 # Galera IST
4444 # SST
];
networking.firewall.allowedUDPPorts = lib.optionals cfg.galeraCluster.enable [
4567 # Galera Cluster
];
};
meta.maintainers = [ lib.maintainers._6543 ];
}

View File

@@ -0,0 +1,719 @@
{
config,
options,
lib,
pkgs,
...
}:
let
cfg = config.services.neo4j;
opt = options.services.neo4j;
certDirOpt = options.services.neo4j.directories.certificates;
isDefaultPathOption =
opt: lib.isOption opt && opt.type == lib.types.path && opt.highestPrio >= 1500;
sslPolicies = lib.mapAttrsToList (name: conf: ''
dbms.ssl.policy.${name}.allow_key_generation=${lib.boolToString conf.allowKeyGeneration}
dbms.ssl.policy.${name}.base_directory=${conf.baseDirectory}
${lib.optionalString (conf.ciphers != null) ''
dbms.ssl.policy.${name}.ciphers=${lib.concatStringsSep "," conf.ciphers}
''}
dbms.ssl.policy.${name}.client_auth=${conf.clientAuth}
${
if lib.length (lib.splitString "/" conf.privateKey) > 1 then
"dbms.ssl.policy.${name}.private_key=${conf.privateKey}"
else
"dbms.ssl.policy.${name}.private_key=${conf.baseDirectory}/${conf.privateKey}"
}
${
if lib.length (lib.splitString "/" conf.privateKey) > 1 then
"dbms.ssl.policy.${name}.public_certificate=${conf.publicCertificate}"
else
"dbms.ssl.policy.${name}.public_certificate=${conf.baseDirectory}/${conf.publicCertificate}"
}
dbms.ssl.policy.${name}.revoked_dir=${conf.revokedDir}
dbms.ssl.policy.${name}.tls_versions=${lib.concatStringsSep "," conf.tlsVersions}
dbms.ssl.policy.${name}.trust_all=${lib.boolToString conf.trustAll}
dbms.ssl.policy.${name}.trusted_dir=${conf.trustedDir}
'') cfg.ssl.policies;
serverConfig = pkgs.writeText "neo4j.conf" ''
# General
server.default_listen_address=${cfg.defaultListenAddress}
server.databases.default_to_read_only=${lib.boolToString cfg.readOnly}
${lib.optionalString (cfg.workerCount > 0) ''
dbms.threads.worker_count=${toString cfg.workerCount}
''}
# Directories (readonly)
# dbms.directories.certificates=${cfg.directories.certificates}
server.directories.plugins=${cfg.directories.plugins}
server.directories.lib=${cfg.package}/share/neo4j/lib
${lib.optionalString (cfg.constrainLoadCsv) ''
server.directories.import=${cfg.directories.imports}
''}
# Directories (read and write)
server.directories.data=${cfg.directories.data}
server.directories.logs=${cfg.directories.home}/logs
server.directories.run=${cfg.directories.home}/run
# HTTP Connector
server.http.enabled=${lib.boolToString cfg.http.enable}
server.http.listen_address=${cfg.http.listenAddress}
server.http.advertised_address=${cfg.http.advertisedAddress}
# HTTPS Connector
server.https.enabled=${lib.boolToString cfg.https.enable}
server.https.listen_address=${cfg.https.listenAddress}
server.https.advertised_address=${cfg.https.advertisedAddress}
# BOLT Connector
server.bolt.enabled=${lib.boolToString cfg.bolt.enable}
server.bolt.listen_address=${cfg.bolt.listenAddress}
server.bolt.advertised_address=${cfg.bolt.advertisedAddress}
server.bolt.tls_level=${cfg.bolt.tlsLevel}
# SSL Policies
${lib.concatStringsSep "\n" sslPolicies}
# Default retention policy from neo4j.conf
db.tx_log.rotation.retention_policy=1 days
# Default JVM parameters from neo4j.conf
server.jvm.additional=-XX:+UseG1GC
server.jvm.additional=-XX:-OmitStackTraceInFastThrow
server.jvm.additional=-XX:+AlwaysPreTouch
server.jvm.additional=-XX:+UnlockExperimentalVMOptions
server.jvm.additional=-XX:+TrustFinalNonStaticFields
server.jvm.additional=-XX:+DisableExplicitGC
server.jvm.additional=-Djdk.tls.ephemeralDHKeySize=2048
server.jvm.additional=-Djdk.tls.rejectClientInitiatedRenegotiation=true
server.jvm.additional=-Dunsupported.dbms.udc.source=tarball
#server.memory.off_heap.transaction_max_size=12000m
#server.memory.heap.max_size=12000m
#server.memory.pagecache.size=4g
#server.tx_state.max_off_heap_memory=8000m
# Extra Configuration
${cfg.extraServerConfig}
'';
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "neo4j" "host" ]
[ "services" "neo4j" "defaultListenAddress" ]
)
(lib.mkRenamedOptionModule
[ "services" "neo4j" "listenAddress" ]
[ "services" "neo4j" "defaultListenAddress" ]
)
(lib.mkRenamedOptionModule
[ "services" "neo4j" "enableBolt" ]
[ "services" "neo4j" "bolt" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "neo4j" "enableHttps" ]
[ "services" "neo4j" "https" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "neo4j" "certDir" ]
[ "services" "neo4j" "directories" "certificates" ]
)
(lib.mkRenamedOptionModule
[ "services" "neo4j" "dataDir" ]
[ "services" "neo4j" "directories" "home" ]
)
(lib.mkRemovedOptionModule [
"services"
"neo4j"
"port"
] "Use services.neo4j.http.listenAddress instead.")
(lib.mkRemovedOptionModule [
"services"
"neo4j"
"boltPort"
] "Use services.neo4j.bolt.listenAddress instead.")
(lib.mkRemovedOptionModule [
"services"
"neo4j"
"httpsPort"
] "Use services.neo4j.https.listenAddress instead.")
(lib.mkRemovedOptionModule [
"services"
"neo4j"
"shell"
"enabled"
] "shell.enabled was removed upstream")
(lib.mkRemovedOptionModule [
"services"
"neo4j"
"udc"
"enabled"
] "udc.enabled was removed upstream")
];
###### interface
options.services.neo4j = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable Neo4j Community Edition.
'';
};
constrainLoadCsv = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Sets the root directory for file URLs used with the Cypher
`LOAD CSV` clause to be that defined by
{option}`directories.imports`. It restricts
access to only those files within that directory and its
subdirectories.
Setting this option to `false` introduces
possible security problems.
'';
};
defaultListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = ''
Default network interface to listen for incoming connections. To
listen for connections on all interfaces, use "0.0.0.0".
Specifies the default IP address and address part of connector
specific {option}`listenAddress` options. To bind specific
connectors to a specific network interfaces, specify the entire
{option}`listenAddress` option for that connector.
'';
};
extraServerConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra configuration for Neo4j Community server. Refer to the
[complete reference](https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/)
of Neo4j configuration settings.
'';
};
package = lib.mkPackageOption pkgs "neo4j" { };
readOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Only allow read operations from this Neo4j instance.
'';
};
workerCount = lib.mkOption {
type = lib.types.ints.between 0 44738;
default = 0;
description = ''
Number of Neo4j worker threads, where the default of
`0` indicates a worker count equal to the number of
available processors.
'';
};
bolt = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable the BOLT connector for Neo4j. Setting this option to
`false` will stop Neo4j from listening for incoming
connections on the BOLT port (7687 by default).
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = ":7687";
description = ''
Neo4j listen address for BOLT traffic. The listen address is
expressed in the format `<ip-address>:<port-number>`.
'';
};
advertisedAddress = lib.mkOption {
type = lib.types.str;
default = cfg.bolt.listenAddress;
defaultText = lib.literalExpression "config.${opt.bolt.listenAddress}";
description = ''
Neo4j advertised address for BOLT traffic. The advertised address is
expressed in the format `<ip-address>:<port-number>`.
'';
};
sslPolicy = lib.mkOption {
type = lib.types.str;
default = "legacy";
description = ''
Neo4j SSL policy for BOLT traffic.
The legacy policy is a special policy which is not defined in
the policy configuration section, but rather derives from
{option}`directories.certificates` and
associated files (by default: {file}`neo4j.key` and
{file}`neo4j.cert`). Its use will be deprecated.
Note: This connector must be configured to support/require
SSL/TLS for the legacy policy to actually be utilized. See
{option}`bolt.tlsLevel`.
'';
};
tlsLevel = lib.mkOption {
type = lib.types.enum [
"REQUIRED"
"OPTIONAL"
"DISABLED"
];
default = "OPTIONAL";
description = ''
SSL/TSL requirement level for BOLT traffic.
'';
};
};
directories = {
certificates = lib.mkOption {
type = lib.types.path;
default = "${cfg.directories.home}/certificates";
defaultText = lib.literalExpression ''"''${config.${opt.directories.home}}/certificates"'';
description = ''
Directory for storing certificates to be used by Neo4j for
TLS connections.
When setting this directory to something other than its default,
ensure the directory's existence, and that read/write permissions are
given to the Neo4j daemon user `neo4j`.
Note that changing this directory from its default will prevent
the directory structure required for each SSL policy from being
automatically generated. A policy's directory structure as defined by
its {option}`baseDirectory`,{option}`revokedDir` and
{option}`trustedDir` must then be setup manually. The
existence of these directories is mandatory, as well as the presence
of the certificate file and the private key. Ensure the correct
permissions are set on these directories and files.
'';
};
data = lib.mkOption {
type = lib.types.path;
default = "${cfg.directories.home}/data";
defaultText = lib.literalExpression ''"''${config.${opt.directories.home}}/data"'';
description = ''
Path of the data directory. You must not configure more than one
Neo4j installation to use the same data directory.
When setting this directory to something other than its default,
ensure the directory's existence, and that read/write permissions are
given to the Neo4j daemon user `neo4j`.
'';
};
home = lib.mkOption {
type = lib.types.path;
default = "/var/lib/neo4j";
description = ''
Path of the Neo4j home directory. Other default directories are
subdirectories of this path. This directory will be created if
non-existent, and its ownership will be {command}`chown` to
the Neo4j daemon user `neo4j`.
'';
};
imports = lib.mkOption {
type = lib.types.path;
default = "${cfg.directories.home}/import";
defaultText = lib.literalExpression ''"''${config.${opt.directories.home}}/import"'';
description = ''
The root directory for file URLs used with the Cypher
`LOAD CSV` clause. Only meaningful when
{option}`constrainLoadCvs` is set to
`true`.
When setting this directory to something other than its default,
ensure the directory's existence, and that read permission is
given to the Neo4j daemon user `neo4j`.
'';
};
plugins = lib.mkOption {
type = lib.types.path;
default = "${cfg.directories.home}/plugins";
defaultText = lib.literalExpression ''"''${config.${opt.directories.home}}/plugins"'';
description = ''
Path of the database plugin directory. Compiled Java JAR files that
contain database procedures will be loaded if they are placed in
this directory.
When setting this directory to something other than its default,
ensure the directory's existence, and that read permission is
given to the Neo4j daemon user `neo4j`.
'';
};
};
http = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable the HTTP connector for Neo4j. Setting this option to
`false` will stop Neo4j from listening for incoming
connections on the HTTPS port (7474 by default).
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = ":7474";
description = ''
Neo4j listen address for HTTP traffic. The listen address is
expressed in the format `<ip-address>:<port-number>`.
'';
};
advertisedAddress = lib.mkOption {
type = lib.types.str;
default = cfg.http.listenAddress;
defaultText = lib.literalExpression "config.${opt.http.listenAddress}";
description = ''
Neo4j advertised address for HTTP traffic. The advertised address is
expressed in the format `<ip-address>:<port-number>`.
'';
};
};
https = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable the HTTPS connector for Neo4j. Setting this option to
`false` will stop Neo4j from listening for incoming
connections on the HTTPS port (7473 by default).
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = ":7473";
description = ''
Neo4j listen address for HTTPS traffic. The listen address is
expressed in the format `<ip-address>:<port-number>`.
'';
};
advertisedAddress = lib.mkOption {
type = lib.types.str;
default = cfg.https.listenAddress;
defaultText = lib.literalExpression "config.${opt.https.listenAddress}";
description = ''
Neo4j advertised address for HTTPS traffic. The advertised address is
expressed in the format `<ip-address>:<port-number>`.
'';
};
sslPolicy = lib.mkOption {
type = lib.types.str;
default = "legacy";
description = ''
Neo4j SSL policy for HTTPS traffic.
The legacy policy is a special policy which is not defined in the
policy configuration section, but rather derives from
{option}`directories.certificates` and
associated files (by default: {file}`neo4j.key` and
{file}`neo4j.cert`). Its use will be deprecated.
'';
};
};
shell = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable a remote shell server which Neo4j Shell clients can log in to.
Only applicable to {command}`neo4j-shell`.
'';
};
};
ssl.policies = lib.mkOption {
type =
with lib.types;
attrsOf (
submodule (
{
name,
config,
options,
...
}:
{
options = {
allowKeyGeneration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allows the generation of a private key and associated self-signed
certificate. Only performed when both objects cannot be found for
this policy. It is recommended to turn this off again after keys
have been generated.
The public certificate is required to be duplicated to the
directory holding trusted certificates as defined by the
{option}`trustedDir` option.
Keys should in general be generated and distributed offline by a
trusted certificate authority and not by utilizing this mode.
'';
};
baseDirectory = lib.mkOption {
type = lib.types.path;
default = "${cfg.directories.certificates}/${name}";
defaultText = lib.literalExpression ''"''${config.${opt.directories.certificates}}/''${name}"'';
description = ''
The mandatory base directory for cryptographic objects of this
policy. This path is only automatically generated when this
option as well as {option}`directories.certificates` are
left at their default. Ensure read/write permissions are given
to the Neo4j daemon user `neo4j`.
It is also possible to override each individual
configuration with absolute paths. See the
{option}`privateKey` and {option}`publicCertificate`
policy options.
'';
};
ciphers = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
Restrict the allowed ciphers of this policy to those defined
here. The default ciphers are those of the JVM platform.
'';
};
clientAuth = lib.mkOption {
type = lib.types.enum [
"NONE"
"OPTIONAL"
"REQUIRE"
];
default = "REQUIRE";
description = ''
The client authentication stance for this policy.
'';
};
privateKey = lib.mkOption {
type = lib.types.str;
default = "private.key";
description = ''
The name of private PKCS #8 key file for this policy to be found
in the {option}`baseDirectory`, or the absolute path to
the key file. It is mandatory that a key can be found or generated.
'';
};
publicCertificate = lib.mkOption {
type = lib.types.str;
default = "public.crt";
description = ''
The name of public X.509 certificate (chain) file in PEM format
for this policy to be found in the {option}`baseDirectory`,
or the absolute path to the certificate file. It is mandatory
that a certificate can be found or generated.
The public certificate is required to be duplicated to the
directory holding trusted certificates as defined by the
{option}`trustedDir` option.
'';
};
revokedDir = lib.mkOption {
type = lib.types.path;
default = "${config.baseDirectory}/revoked";
defaultText = lib.literalExpression ''"''${config.${options.baseDirectory}}/revoked"'';
description = ''
Path to directory of CRLs (Certificate Revocation Lists) in
PEM format. Must be an absolute path. The existence of this
directory is mandatory and will need to be created manually when:
setting this option to something other than its default; setting
either this policy's {option}`baseDirectory` or
{option}`directories.certificates` to something other than
their default. Ensure read/write permissions are given to the
Neo4j daemon user `neo4j`.
'';
};
tlsVersions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "TLSv1.2" ];
description = ''
Restrict the TLS protocol versions of this policy to those
defined here.
'';
};
trustAll = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Makes this policy trust all remote parties. Enabling this is not
recommended and the policy's trusted directory will be ignored.
Use of this mode is discouraged. It would offer encryption but
no security.
'';
};
trustedDir = lib.mkOption {
type = lib.types.path;
default = "${config.baseDirectory}/trusted";
defaultText = lib.literalExpression ''"''${config.${options.baseDirectory}}/trusted"'';
description = ''
Path to directory of X.509 certificates in PEM format for
trusted parties. Must be an absolute path. The existence of this
directory is mandatory and will need to be created manually when:
setting this option to something other than its default; setting
either this policy's {option}`baseDirectory` or
{option}`directories.certificates` to something other than
their default. Ensure read/write permissions are given to the
Neo4j daemon user `neo4j`.
The public certificate as defined by
{option}`publicCertificate` is required to be duplicated
to this directory.
'';
};
directoriesToCreate = lib.mkOption {
type = lib.types.listOf lib.types.path;
internal = true;
readOnly = true;
description = ''
Directories of this policy that will be created automatically
when the certificates directory is left at its default value.
This includes all options of type path that are left at their
default value.
'';
};
};
config.directoriesToCreate = lib.optionals (
certDirOpt.highestPrio >= 1500 && options.baseDirectory.highestPrio >= 1500
) (map (opt: opt.value) (lib.filter isDefaultPathOption (lib.attrValues options)));
}
)
);
default = { };
description = ''
Defines the SSL policies for use with Neo4j connectors. Each attribute
of this set defines a policy, with the attribute name defining the name
of the policy and its namespace. Refer to the operations manual section
on Neo4j's
[SSL Framework](https://neo4j.com/docs/operations-manual/current/security/ssl-framework/)
for further details.
'';
};
};
###### implementation
config =
let
# Assertion helpers
policyNameList = lib.attrNames cfg.ssl.policies;
validPolicyNameList = [ "legacy" ] ++ policyNameList;
validPolicyNameString = lib.concatStringsSep ", " validPolicyNameList;
# Capture various directories left at their default so they can be created.
defaultDirectoriesToCreate = map (opt: opt.value) (
lib.filter isDefaultPathOption (lib.attrValues options.services.neo4j.directories)
);
policyDirectoriesToCreate = lib.concatMap (pol: pol.directoriesToCreate) (
lib.attrValues cfg.ssl.policies
);
in
lib.mkIf cfg.enable {
assertions = [
{
assertion = !lib.elem "legacy" policyNameList;
message = "The policy 'legacy' is special to Neo4j, and its name is reserved.";
}
{
assertion = lib.elem cfg.bolt.sslPolicy validPolicyNameList;
message = "Invalid policy assigned: `services.neo4j.bolt.sslPolicy = \"${cfg.bolt.sslPolicy}\"`, defined policies are: ${validPolicyNameString}";
}
{
assertion = lib.elem cfg.https.sslPolicy validPolicyNameList;
message = "Invalid policy assigned: `services.neo4j.https.sslPolicy = \"${cfg.https.sslPolicy}\"`, defined policies are: ${validPolicyNameString}";
}
];
systemd.services.neo4j = {
description = "Neo4j Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
NEO4J_HOME = "${cfg.directories.home}";
NEO4J_CONF = "${cfg.directories.home}/conf";
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/neo4j console";
User = "neo4j";
PermissionsStartOnly = true;
LimitNOFILE = 40000;
};
preStart = ''
# Directories Setup
# Always ensure home exists with nested conf, logs directories.
mkdir -m 0700 -p ${cfg.directories.home}/{conf,logs}
# Create other sub-directories and policy directories that have been left at their default.
${lib.concatMapStringsSep "\n" (dir: ''
mkdir -m 0700 -p ${dir}
'') (defaultDirectoriesToCreate ++ policyDirectoriesToCreate)}
# Place the configuration where Neo4j can find it.
ln -fs ${serverConfig} ${cfg.directories.home}/conf/neo4j.conf
# Ensure neo4j user ownership
chown -R neo4j ${cfg.directories.home}
'';
};
environment.systemPackages = [ cfg.package ];
users.users.neo4j = {
isSystemUser = true;
group = "neo4j";
description = "Neo4j daemon user";
home = cfg.directories.home;
};
users.groups.neo4j = { };
};
meta = {
maintainers = with lib.maintainers; [ patternspandemic ];
};
}

View File

@@ -0,0 +1,398 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.openldap;
openldap = cfg.package;
configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
ldapValueType =
let
# Can't do types.either with multiple non-overlapping submodules, so define our own
singleLdapValueType = lib.mkOptionType {
name = "LDAP";
# TODO: It would be nice to define a { secret = ...; } option, using
# systemd's LoadCredentials for secrets. That would remove the last
# barrier to using DynamicUser for openldap. This is blocked on
# systemd/systemd#19604
description = ''
LDAP value - either a string, or an attrset containing
`path` or `base64` for included
values or base-64 encoded values respectively.
'';
check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
merge = lib.mergeEqualOption;
};
in
# We don't coerce to lists of single values, as some values must be unique
lib.types.either singleLdapValueType (lib.types.listOf singleLdapValueType);
ldapAttrsType =
let
options = {
attrs = lib.mkOption {
type = lib.types.attrsOf ldapValueType;
default = { };
description = "Attributes of the parent entry.";
};
children = lib.mkOption {
# Hide the child attributes, to avoid infinite recursion in e.g. documentation
# Actual Nix evaluation is lazy, so this is not an issue there
type =
let
hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
in
lib.types.attrsOf (lib.types.submodule { options = hiddenOptions; });
default = { };
description = "Child entries of the current entry, with recursively the same structure.";
example = lib.literalExpression ''
{
"cn=schema" = {
# The attribute used in the DN must be defined
attrs = { cn = "schema"; };
children = {
# This entry's DN is expanded to "cn=foo,cn=schema"
"cn=foo" = { ... };
};
# These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
includes = [ ... ];
};
}
'';
};
includes = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
LDIF files to include after the parent's attributes but before its children.
'';
};
};
in
lib.types.submodule { inherit options; };
valueToLdif =
attr: values:
let
listValues = if lib.isList values then values else lib.singleton values;
in
map (
value:
if lib.isAttrs value then
if lib.hasAttr "path" value then "${attr}:< file://${value.path}" else "${attr}:: ${value.base64}"
else
"${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
) listValues;
attrsToLdif =
dn:
{
attrs,
children,
includes,
...
}:
[
''
dn: ${dn}
${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
''
]
++ (map (path: "include: file://${path}\n") includes)
++ (lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children));
in
{
options = {
services.openldap = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the ldap server.";
};
package = lib.mkPackageOption pkgs "openldap" {
extraDescription = ''
This can be used to, for example, set an OpenLDAP package
with custom overrides to enable modules or other
functionality.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "openldap";
description = "User account under which slapd runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "openldap";
description = "Group account under which slapd runs.";
};
urlList = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "ldap:///" ];
description = "URL list slapd should listen on.";
example = [ "ldaps:///" ];
};
settings = lib.mkOption {
type = ldapAttrsType;
description = "Configuration for OpenLDAP, in OLC format";
example = lib.literalExpression ''
{
attrs.olcLogLevel = [ "stats" ];
children = {
"cn=schema".includes = [
"''${pkgs.openldap}/etc/schema/core.ldif"
"''${pkgs.openldap}/etc/schema/cosine.ldif"
"''${pkgs.openldap}/etc/schema/inetorgperson.ldif"
];
"olcDatabase={-1}frontend" = {
attrs = {
objectClass = "olcDatabaseConfig";
olcDatabase = "{-1}frontend";
olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
};
};
"olcDatabase={0}config" = {
attrs = {
objectClass = "olcDatabaseConfig";
olcDatabase = "{0}config";
olcAccess = [ "{0}to * by * none break" ];
};
};
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/ldap";
olcDbIndex = [
"objectClass eq"
"cn pres,eq"
"uid pres,eq"
"sn pres,eq,subany"
];
olcSuffix = "dc=example,dc=com";
olcAccess = [ "{0}to * by * read break" ];
};
};
};
};
'';
};
# This option overrides settings
configDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Use this config directory instead of generating one from the
`settings` option. Overrides all NixOS settings.
'';
example = "/var/lib/openldap/slapd.d";
};
mutableConfig = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to allow writable on-line configuration. If
`true`, the NixOS settings will only be used to
initialize the OpenLDAP configuration if it does not exist, and are
subsequently ignored.
'';
};
declarativeContents = lib.mkOption {
type = with lib.types; attrsOf lines;
default = { };
description = ''
Declarative contents for the LDAP database, in LDIF format by suffix.
All data will be erased when starting the LDAP server. Modifications
to the database are not prevented, they are just dropped on the next
reboot of the server. Performance-wise the database and indexes are
rebuilt on each server startup, so this will slow down server startup,
especially with large databases.
Note that the root of the DB must be defined in
`services.openldap.settings` and the
`olcDbDirectory` must begin with
`"/var/lib/openldap"`.
'';
example = lib.literalExpression ''
{
"dc=example,dc=org" = '''
dn= dn: dc=example,dc=org
objectClass: domain
dc: example
dn: ou=users,dc=example,dc=org
objectClass = organizationalUnit
ou: users
# ...
''';
}
'';
};
};
};
meta.maintainers = with lib.maintainers; [ kwohlfahrt ];
config =
let
dbSettings = lib.mapAttrs' (name: { attrs, ... }: lib.nameValuePair attrs.olcSuffix attrs) (
lib.filterAttrs (
name: { attrs, ... }: (lib.hasPrefix "olcDatabase=" name) && attrs ? olcSuffix
) cfg.settings.children
);
settingsFile = pkgs.writeText "config.ldif" (
lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings)
);
writeConfig = pkgs.writeShellScript "openldap-config" ''
set -euo pipefail
${lib.optionalString (!cfg.mutableConfig) ''
chmod -R u+w ${configDir}
rm -rf ${configDir}/*
''}
if [ ! -e "${configDir}/cn=config.ldif" ]; then
${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
fi
chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir}
'';
contentsFiles = lib.mapAttrs (dn: ldif: pkgs.writeText "${dn}.ldif" ldif) cfg.declarativeContents;
writeContents = pkgs.writeShellScript "openldap-load" ''
set -euo pipefail
rm -rf $2/*
${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3
'';
in
lib.mkIf cfg.enable {
assertions = [
{
assertion = (cfg.declarativeContents != { }) -> cfg.configDir == null;
message = ''
Declarative DB contents (${lib.attrNames cfg.declarativeContents}) are not
supported with user-managed configuration.
'';
}
]
++ (map (dn: {
assertion = (lib.getAttr dn dbSettings) ? "olcDbDirectory";
# olcDbDirectory is necessary to prepopulate database using `slapadd`.
message = ''
Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have
`olcDbDirectory` configured.
'';
}) (lib.attrNames cfg.declarativeContents))
++ (lib.mapAttrsToList (
dn:
{
olcDbDirectory ? null,
...
}:
{
# For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering
# directories with `declarativeContents`.
assertion =
(olcDbDirectory != null)
-> (
(lib.hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/")
);
message = ''
Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of
`/var/lib/openldap/`.
'';
}
) dbSettings);
environment.systemPackages = [ openldap ];
# Literal attributes must always be set
services.openldap.settings = {
attrs = {
objectClass = "olcGlobal";
cn = "config";
};
children."cn=schema".attrs = {
cn = "schema";
objectClass = "olcSchemaConfig";
};
};
systemd.services.openldap = {
description = "OpenLDAP Server Daemon";
documentation = [
"man:slapd"
"man:slapd-config"
"man:slapd-mdb"
];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStartPre = [
"!${pkgs.coreutils}/bin/mkdir -p ${configDir}"
"+${pkgs.coreutils}/bin/chown $USER ${configDir}"
]
++ (lib.optional (cfg.configDir == null) writeConfig)
++ (lib.mapAttrsToList (
dn: content:
lib.escapeShellArgs [
writeContents
dn
(lib.getAttr dn dbSettings).olcDbDirectory
content
]
) contentsFiles)
++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ];
ExecStart = lib.escapeShellArgs [
"${openldap}/libexec/slapd"
"-d"
"0"
"-F"
configDir
"-h"
(lib.concatStringsSep " " cfg.urlList)
];
Type = "notify";
# Fixes an error where openldap attempts to notify from a thread
# outside the main process:
# Got notification message from PID 6378, but reception only permitted for main PID 6377
NotifyAccess = "all";
RuntimeDirectory = "openldap";
StateDirectory = [
"openldap"
]
++ (map ({ olcDbDirectory, ... }: lib.removePrefix "/var/lib/" olcDbDirectory) (
lib.attrValues dbSettings
));
StateDirectoryMode = "700";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
};
};
users.users = lib.optionalAttrs (cfg.user == "openldap") {
openldap = {
group = cfg.group;
isSystemUser = true;
};
};
users.groups = lib.optionalAttrs (cfg.group == "openldap") {
openldap = { };
};
};
}

View File

@@ -0,0 +1,97 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.opentsdb;
configFile = pkgs.writeText "opentsdb.conf" cfg.config;
in
{
###### interface
options = {
services.opentsdb = {
enable = lib.mkEnableOption "OpenTSDB";
package = lib.mkPackageOption pkgs "opentsdb" { };
user = lib.mkOption {
type = lib.types.str;
default = "opentsdb";
description = ''
User account under which OpenTSDB runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "opentsdb";
description = ''
Group account under which OpenTSDB runs.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 4242;
description = ''
Which port OpenTSDB listens on.
'';
};
config = lib.mkOption {
type = lib.types.lines;
default = ''
tsd.core.auto_create_metrics = true
tsd.http.request.enable_chunked = true
'';
description = ''
The contents of OpenTSDB's configuration file
'';
};
};
};
###### implementation
config = lib.mkIf config.services.opentsdb.enable {
systemd.services.opentsdb = {
description = "OpenTSDB Server";
wantedBy = [ "multi-user.target" ];
requires = [ "hbase.service" ];
environment.JAVA_HOME = "${pkgs.jre}";
path = [ pkgs.gnuplot ];
preStart = ''
COMPRESSION=NONE HBASE_HOME=${config.services.hbase.package} ${cfg.package}/share/opentsdb/tools/create_table.sh
'';
serviceConfig = {
PermissionsStartOnly = true;
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/tsdb tsd --staticroot=${cfg.package}/share/opentsdb/static --cachedir=/tmp/opentsdb --port=${toString cfg.port} --config=${configFile}";
};
};
users.users.opentsdb = {
description = "OpenTSDB Server user";
group = "opentsdb";
uid = config.ids.uids.opentsdb;
};
users.groups.opentsdb.gid = config.ids.gids.opentsdb;
};
}

View File

@@ -0,0 +1,439 @@
{
config,
lib,
utils,
pkgs,
...
}:
let
cfg = config.services.pgbouncer;
settingsFormat = pkgs.formats.ini { };
configFile = settingsFormat.generate "pgbouncer.ini" (
lib.filterAttrsRecursive (_: v: v != null) cfg.settings
);
configPath = "pgbouncer/pgbouncer.ini";
in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "pgbouncer" "logFile" ] ''
`services.pgbouncer.logFile` has been removed, use `services.pgbouncer.settings.pgbouncer.logfile`
instead.
Please note that the new option expects an absolute path
whereas the old option accepted paths relative to pgbouncer's home dir.
'')
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "listenAddress" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "listen_addr" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "listenPort" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "listen_port" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "poolMode" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "pool_mode" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "maxClientConn" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "max_client_conn" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "defaultPoolSize" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "default_pool_size" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "maxDbConnections" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "max_db_connections" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "maxUserConnections" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "max_user_connections" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "ignoreStartupParameters" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "ignore_startup_parameters" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "databases" ]
[ "services" "pgbouncer" "settings" "databases" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "users" ]
[ "services" "pgbouncer" "settings" "users" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "peers" ]
[ "services" "pgbouncer" "settings" "peers" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "authType" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "auth_type" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "authHbaFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "auth_hba_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "authFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "auth_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "authUser" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "auth_user" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "authQuery" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "auth_query" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "authDbname" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "auth_dbname" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "adminUsers" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "admin_users" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "statsUsers" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "stats_users" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "verbose" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "verbose" ]
)
(lib.mkChangedOptionModule
[ "services" "pgbouncer" "syslog" "enable" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "syslog" ]
(
config:
let
enable = lib.getAttrFromPath [ "services" "pgbouncer" "syslog" "enable" ] config;
in
if enable then 1 else 0
)
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "syslog" "syslogIdent" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "syslog_ident" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "syslog" "syslogFacility" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "syslog_facility" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "client" "sslmode" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "client_tls_sslmode" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "client" "keyFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "client_tls_key_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "client" "certFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "client_tls_cert_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "client" "caFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "client_tls_ca_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "server" "sslmode" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "server_tls_sslmode" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "server" "keyFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "server_tls_key_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "server" "certFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "server_tls_cert_file" ]
)
(lib.mkRenamedOptionModule
[ "services" "pgbouncer" "tls" "server" "caFile" ]
[ "services" "pgbouncer" "settings" "pgbouncer" "server_tls_ca_file" ]
)
(lib.mkRemovedOptionModule [
"services"
"pgbouncer"
"extraConfig"
] "Use services.pgbouncer.settings instead.")
];
options.services.pgbouncer = {
enable = lib.mkEnableOption "PostgreSQL connection pooler";
package = lib.mkPackageOption pkgs "pgbouncer" { };
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to automatically open the specified TCP port in the firewall.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
pgbouncer = {
listen_port = lib.mkOption {
type = lib.types.port;
default = 6432;
description = ''
Which port to listen on. Applies to both TCP and Unix sockets.
'';
};
listen_addr = lib.mkOption {
type = lib.types.nullOr lib.types.commas;
example = "*";
default = null;
description = ''
Specifies a list (comma-separated) of addresses where to listen for TCP connections.
You may also use * meaning listen on all addresses.
When not set, only Unix socket connections are accepted.
Addresses can be specified numerically (IPv4/IPv6) or by name.
'';
};
pool_mode = lib.mkOption {
type = lib.types.enum [
"session"
"transaction"
"statement"
];
default = "session";
description = ''
Specifies when a server connection can be reused by other clients.
session
Server is released back to pool after client disconnects. Default.
transaction
Server is released back to pool after transaction finishes.
statement
Server is released back to pool after query finishes.
Transactions spanning multiple statements are disallowed in this mode.
'';
};
max_client_conn = lib.mkOption {
type = lib.types.int;
default = 100;
description = ''
Maximum number of client connections allowed.
When this setting is increased, then the file descriptor limits in the operating system
might also have to be increased. Note that the number of file descriptors potentially
used is more than maxClientConn. If each user connects under its own user name to the server,
the theoretical maximum used is:
maxClientConn + (max pool_size * total databases * total users)
If a database user is specified in the connection string (all users connect under the same user name),
the theoretical maximum is:
maxClientConn + (max pool_size * total databases)
The theoretical maximum should never be reached, unless somebody deliberately crafts a special load for it.
Still, it means you should set the number of file descriptors to a safely high number.
'';
};
default_pool_size = lib.mkOption {
type = lib.types.int;
default = 20;
description = ''
How many server connections to allow per user/database pair.
Can be overridden in the per-database configuration.
'';
};
max_db_connections = lib.mkOption {
type = lib.types.int;
default = 0;
description = ''
Do not allow more than this many server connections per database (regardless of user).
This considers the PgBouncer database that the client has connected to,
not the PostgreSQL database of the outgoing connection.
This can also be set per database in the [databases] section.
Note that when you hit the limit, closing a client connection to one pool will
not immediately allow a server connection to be established for another pool,
because the server connection for the first pool is still open.
Once the server connection closes (due to idle timeout),
a new server connection will immediately be opened for the waiting pool.
0 = unlimited
'';
};
max_user_connections = lib.mkOption {
type = lib.types.int;
default = 0;
description = ''
Do not allow more than this many server connections per user (regardless of database).
This considers the PgBouncer user that is associated with a pool,
which is either the user specified for the server connection
or in absence of that the user the client has connected as.
This can also be set per user in the [users] section.
Note that when you hit the limit, closing a client connection to one pool
will not immediately allow a server connection to be established for another pool,
because the server connection for the first pool is still open.
Once the server connection closes (due to idle timeout), a new server connection
will immediately be opened for the waiting pool.
0 = unlimited
'';
};
ignore_startup_parameters = lib.mkOption {
type = lib.types.nullOr lib.types.commas;
example = "extra_float_digits";
default = null;
description = ''
By default, PgBouncer allows only parameters it can keep track of in startup packets:
client_encoding, datestyle, timezone and standard_conforming_strings.
All others parameters will raise an error.
To allow others parameters, they can be specified here, so that PgBouncer knows that
they are handled by the admin and it can ignore them.
If you need to specify multiple values, use a comma-separated list.
IMPORTANT: When using prometheus-pgbouncer-exporter, you need:
extra_float_digits
<https://github.com/prometheus-community/pgbouncer_exporter#pgbouncer-configuration>
'';
};
};
databases = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
exampledb = "host=/run/postgresql/ port=5432 auth_user=exampleuser dbname=exampledb sslmode=require";
bardb = "host=localhost dbname=bazdb";
foodb = "host=host1.example.com port=5432";
};
description = ''
Detailed information about PostgreSQL database definitions:
<https://www.pgbouncer.org/config.html#section-databases>
'';
};
users = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
user1 = "pool_mode=session";
};
description = ''
Optional.
Detailed information about PostgreSQL user definitions:
<https://www.pgbouncer.org/config.html#section-users>
'';
};
peers = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
"1" = "host=host1.example.com";
"2" = "host=/tmp/pgbouncer-2 port=5555";
};
description = ''
Optional.
Detailed information about PostgreSQL database definitions:
<https://www.pgbouncer.org/config.html#section-peers>
'';
};
};
};
default = { };
description = ''
Configuration for PgBouncer, see <https://www.pgbouncer.org/config.html>
for supported values.
'';
};
# Linux settings
openFilesLimit = lib.mkOption {
type = lib.types.int;
default = 65536;
description = ''
Maximum number of open files.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "pgbouncer";
description = ''
The user pgbouncer is run as.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "pgbouncer";
description = ''
The group pgbouncer is run as.
'';
};
homeDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/pgbouncer";
description = ''
Specifies the home directory.
'';
};
};
config = lib.mkIf cfg.enable {
users.groups.${cfg.group} = { };
users.users.${cfg.user} = {
description = "PgBouncer service user";
group = cfg.group;
home = cfg.homeDir;
createHome = true;
isSystemUser = true;
};
environment.etc.${configPath}.source = configFile;
# Default to RuntimeDirectory instead of /tmp.
services.pgbouncer.settings.pgbouncer.unix_socket_dir = lib.mkDefault "/run/pgbouncer";
systemd.services.pgbouncer = {
description = "PgBouncer - PostgreSQL connection pooler";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
reloadTriggers = [ configFile ];
serviceConfig = {
Type = "notify-reload";
User = cfg.user;
Group = cfg.group;
ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe pkgs.pgbouncer)
"/etc/${configPath}"
];
RuntimeDirectory = "pgbouncer";
LimitNOFILE = cfg.openFilesLimit;
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
(cfg.settings.pgbouncer.listen_port or 6432)
];
};
};
meta.maintainers = [ lib.maintainers._1000101 ];
}

View File

@@ -0,0 +1,212 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.pgmanage;
confFile = pkgs.writeTextFile {
name = "pgmanage.conf";
text = ''
connection_file = ${pgmanageConnectionsFile}
allow_custom_connections = ${builtins.toJSON cfg.allowCustomConnections}
pgmanage_port = ${toString cfg.port}
super_only = ${builtins.toJSON cfg.superOnly}
${lib.optionalString (cfg.loginGroup != null) "login_group = ${cfg.loginGroup}"}
login_timeout = ${toString cfg.loginTimeout}
web_root = ${cfg.package}/etc/pgmanage/web_root
sql_root = ${cfg.sqlRoot}
${lib.optionalString (cfg.tls != null) ''
tls_cert = ${cfg.tls.cert}
tls_key = ${cfg.tls.key}
''}
log_level = ${cfg.logLevel}
'';
};
pgmanageConnectionsFile = pkgs.writeTextFile {
name = "pgmanage-connections.conf";
text = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: conn: "${name}: ${conn}") cfg.connections
);
};
pgmanage = "pgmanage";
in
{
options.services.pgmanage = {
enable = lib.mkEnableOption "PostgreSQL Administration for the web";
package = lib.mkPackageOption pkgs "pgmanage" { };
connections = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
nuc-server = "hostaddr=192.168.0.100 port=5432 dbname=postgres";
mini-server = "hostaddr=127.0.0.1 port=5432 dbname=postgres sslmode=require";
};
description = ''
pgmanage requires at least one PostgreSQL server be defined.
Detailed information about PostgreSQL connection strings is available at:
<https://www.postgresql.org/docs/current/libpq-connect.html>
Note that you should not specify your user name or password. That
information will be entered on the login screen. If you specify a
username or password, it will be removed by pgmanage before attempting to
connect to a database.
'';
};
allowCustomConnections = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This tells pgmanage whether or not to allow anyone to use a custom
connection from the login screen.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = ''
This tells pgmanage what port to listen on for browser requests.
'';
};
localOnly = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
This tells pgmanage whether or not to set the listening socket to local
addresses only.
'';
};
superOnly = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
This tells pgmanage whether or not to only allow super users to
login. The recommended value is true and will restrict users who are not
super users from logging in to any PostgreSQL instance through
pgmanage. Note that a connection will be made to PostgreSQL in order to
test if the user is a superuser.
'';
};
loginGroup = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
This tells pgmanage to only allow users in a certain PostgreSQL group to
login to pgmanage. Note that a connection will be made to PostgreSQL in
order to test if the user is a member of the login group.
'';
};
loginTimeout = lib.mkOption {
type = lib.types.int;
default = 3600;
description = ''
Number of seconds of inactivity before user is automatically logged
out.
'';
};
sqlRoot = lib.mkOption {
type = lib.types.str;
default = "/var/lib/pgmanage";
description = ''
This tells pgmanage where to put the SQL file history. All tabs are saved
to this location so that if you get disconnected from pgmanage you
don't lose your work.
'';
};
tls = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
cert = lib.mkOption {
type = lib.types.str;
description = "TLS certificate";
};
key = lib.mkOption {
type = lib.types.str;
description = "TLS key";
};
};
}
);
default = null;
description = ''
These options tell pgmanage where the TLS Certificate and Key files
reside. If you use these options then you'll only be able to access
pgmanage through a secure TLS connection. These options are only
necessary if you wish to connect directly to pgmanage using a secure TLS
connection. As an alternative, you can set up pgmanage in a reverse proxy
configuration. This allows your web server to terminate the secure
connection and pass on the request to pgmanage. You can find help to set
up this configuration in:
<https://github.com/pgManage/pgManage/blob/master/INSTALL_NGINX.md>
'';
};
logLevel = lib.mkOption {
type = lib.types.enum [
"error"
"warn"
"notice"
"info"
];
default = "error";
description = ''
Verbosity of logs
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.pgmanage = {
description = "pgmanage - PostgreSQL Administration for the web";
wants = [ "postgresql.target" ];
after = [ "postgresql.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = pgmanage;
Group = pgmanage;
ExecStart =
"${cfg.package}/sbin/pgmanage -c ${confFile}"
+ lib.optionalString cfg.localOnly " --local-only=true";
};
};
users = {
users.${pgmanage} = {
name = pgmanage;
group = pgmanage;
home = cfg.sqlRoot;
createHome = true;
isSystemUser = true;
};
groups.${pgmanage} = {
name = pgmanage;
};
};
};
}

View File

@@ -0,0 +1,222 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.postgres-websockets;
# Turns an attrset of libpq connection params:
# {
# dbname = "postgres";
# user = "authenticator";
# }
# into a libpq connection string:
# dbname=postgres user=authenticator
PGWS_DB_URI = lib.pipe cfg.environment.PGWS_DB_URI [
(lib.filterAttrs (_: v: v != null))
(lib.mapAttrsToList (k: v: "${k}='${lib.escape [ "'" "\\" ] v}'"))
(lib.concatStringsSep " ")
];
in
{
meta = {
maintainers = with lib.maintainers; [ wolfgangwalther ];
};
options.services.postgres-websockets = {
enable = lib.mkEnableOption "postgres-websockets";
pgpassFile = lib.mkOption {
type =
with lib.types;
nullOr (pathWith {
inStore = false;
absolute = true;
});
default = null;
example = "/run/keys/db_password";
description = ''
The password to authenticate to PostgreSQL with.
Not needed for peer or trust based authentication.
The file must be a valid `.pgpass` file as described in:
<https://www.postgresql.org/docs/current/libpq-pgpass.html>
In most cases, the following will be enough:
```
*:*:*:*:<password>
```
'';
};
jwtSecretFile = lib.mkOption {
type =
with lib.types;
nullOr (pathWith {
inStore = false;
absolute = true;
});
example = "/run/keys/jwt_secret";
description = ''
Secret used to sign JWT tokens used to open communications channels.
'';
};
environment = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
options = {
PGWS_DB_URI = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
# This should not be used; use pgpassFile instead.
options.password = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
# This should not be used; use pgpassFile instead.
options.passfile = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
};
default = { };
description = ''
libpq connection parameters as documented in:
<https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>
::: {.note}
The `environment.PGWS_DB_URI.password` and `environment.PGWS_DB_URI.passfile` options are blocked.
Use [`pgpassFile`](#opt-services.postgres-websockets.pgpassFile) instead.
:::
'';
example = lib.literalExpression ''
{
host = "localhost";
dbname = "postgres";
}
'';
};
# This should not be used; use jwtSecretFile instead.
PGWS_JWT_SECRET = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
PGWS_HOST = lib.mkOption {
type = with lib.types; nullOr str;
default = "127.0.0.1";
description = ''
Address the server will listen for websocket connections.
'';
};
};
};
default = { };
description = ''
postgres-websockets configuration as defined in:
<https://github.com/diogob/postgres-websockets/blob/master/src/PostgresWebsockets/Config.hs#L71-L87>
`PGWS_DB_URI` is represented as an attribute set, see [`environment.PGWS_DB_URI`](#opt-services.postgres-websockets.environment.PGWS_DB_URI)
::: {.note}
The `environment.PGWS_JWT_SECRET` option is blocked.
Use [`jwtSecretFile`](#opt-services.postgres-websockets.jwtSecretFile) instead.
:::
'';
example = lib.literalExpression ''
{
PGWS_LISTEN_CHANNEL = "my_channel";
PGWS_DB_URI.dbname = "postgres";
}
'';
};
};
config = lib.mkIf cfg.enable {
services.postgres-websockets.environment.PGWS_DB_URI.application_name =
with pkgs.postgres-websockets;
"${pname} ${version}";
systemd.services.postgres-websockets = {
description = "postgres-websockets";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
"postgresql.target"
];
environment =
cfg.environment
// {
inherit PGWS_DB_URI;
PGWS_JWT_SECRET = "@%d/jwt_secret";
}
// lib.optionalAttrs (cfg.pgpassFile != null) {
PGPASSFILE = "%C/postgres-websockets/pgpass";
};
serviceConfig = {
CacheDirectory = "postgres-websockets";
CacheDirectoryMode = "0700";
LoadCredential = [
"jwt_secret:${cfg.jwtSecretFile}"
]
++ lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}";
Restart = "always";
User = "postgres-websockets";
# Hardening
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateIPC = true;
PrivateMounts = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "" ];
UMask = "0077";
};
# Copy the pgpass file to different location, to have it report mode 0400.
# Fixes: https://github.com/systemd/systemd/issues/29435
script = ''
if [ -f "$CREDENTIALS_DIRECTORY/pgpass" ]; then
cp -f "$CREDENTIALS_DIRECTORY/pgpass" "$CACHE_DIRECTORY/pgpass"
fi
exec ${lib.getExe pkgs.postgres-websockets}
'';
};
};
}

View File

@@ -0,0 +1,451 @@
# PostgreSQL {#module-postgresql}
<!-- FIXME: render nicely -->
<!-- FIXME: source can be added automatically -->
*Source:* {file}`modules/services/databases/postgresql.nix`
*Upstream documentation:* <https://www.postgresql.org/docs/>
<!-- FIXME: more stuff, like maintainer? -->
PostgreSQL is an advanced, free, relational database.
<!-- MORE -->
## Configuring {#module-services-postgres-configuring}
To enable PostgreSQL, add the following to your {file}`configuration.nix`:
```nix
{
services.postgresql.enable = true;
services.postgresql.package = pkgs.postgresql_15;
}
```
The default PostgreSQL version is approximately the latest major version available on the NixOS release matching your [`system.stateVersion`](#opt-system.stateVersion).
This is because PostgreSQL upgrades require a manual migration process (see below).
Hence, upgrades must happen by setting [`services.postgresql.package`](#opt-services.postgresql.package) explicitly.
<!--
After running {command}`nixos-rebuild`, you can verify
whether PostgreSQL works by running {command}`psql`:
```ShellSession
$ psql
psql (9.2.9)
Type "help" for help.
alice=>
```
-->
By default, PostgreSQL stores its databases in {file}`/var/lib/postgresql/$psqlSchema`. You can override this using [](#opt-services.postgresql.dataDir), e.g.
```nix
{ services.postgresql.dataDir = "/data/postgresql"; }
```
## Initializing {#module-services-postgres-initializing}
As of NixOS 24.05,
`services.postgresql.ensureUsers.*.ensurePermissions` has been
removed, after a change to default permissions in PostgreSQL 15
invalidated most of its previous use cases:
- In psql < 15, `ALL PRIVILEGES` used to include `CREATE TABLE`, where
in psql >= 15 that would be a separate permission
- psql >= 15 instead gives only the database owner create permissions
- Even on psql < 15 (or databases migrated to >= 15), it is
recommended to manually assign permissions along these lines
- <https://www.postgresql.org/docs/release/15.0/>
- <https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PRIV>
### Assigning ownership {#module-services-postgres-initializing-ownership}
Usually, the database owner should be a database user of the same
name. This can be done with
`services.postgresql.ensureUsers.*.ensureDBOwnership = true;`.
If the database user name equals the connecting system user name,
postgres by default will accept a passwordless connection via unix
domain socket. This makes it possible to run many postgres-backed
services without creating any database secrets at all.
### Assigning extra permissions {#module-services-postgres-initializing-extra-permissions}
For many cases, it will be enough to have the database user be the
owner. Until `services.postgresql.ensureUsers.*.ensurePermissions` has
been re-thought, if more users need access to the database, please use
one of the following approaches:
**WARNING:** `services.postgresql.initialScript` is not recommended
for `ensurePermissions` replacement, as that is *only run on first
start of PostgreSQL*.
**NOTE:** all of these methods may be obsoleted, when `ensure*` is
reworked, but it is expected that they will stay viable for running
database migrations.
**NOTE:** please make sure that any added migrations are idempotent (re-runnable).
#### in database's setup `postStart` {#module-services-postgres-initializing-extra-permissions-superuser-post-start}
`ensureUsers` is run in `postgresql-setup`, so this is where `postStart` must be added to:
```nix
{
systemd.services.postgresql-setup.postStart = ''
psql service1 -c 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO "extraUser1"'
psql service1 -c 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "extraUser1"'
# ....
'';
}
```
#### in intermediate oneshot service {#module-services-postgres-initializing-extra-permissions-superuser-oneshot}
Make sure to run this service after `postgresql.target`, not `postgresql.service`.
They differ in two aspects:
- `postgresql.target` includes `postgresql-setup`, so users managed via `ensureUsers` are already created.
- `postgresql.target` will wait until PostgreSQL is in read-write mode after restoring from backup, while `postgresql.service` will already be ready when PostgreSQL is still recovering in read-only mode.
Both can lead to unexpected errors either during initial database creation or restore, when using `postgresql.service`.
```nix
{
systemd.services."migrate-service1-db1" = {
serviceConfig.Type = "oneshot";
requiredBy = "service1.service";
before = "service1.service";
after = "postgresql.target";
serviceConfig.User = "postgres";
environment.PGPORT = toString services.postgresql.settings.port;
path = [ postgresql ];
script = ''
psql service1 -c 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO "extraUser1"'
psql service1 -c 'GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "extraUser1"'
# ....
'';
};
}
```
## Authentication {#module-services-postgres-authentication}
Local connections are made through unix sockets by default and support [peer authentication](https://www.postgresql.org/docs/current/auth-peer.html).
This allows system users to login with database roles of the same name.
For example, the `postgres` system user is allowed to login with the database role `postgres`.
System users and database roles might not always match.
In this case, to allow access for a service, you can create a [user name map](https://www.postgresql.org/docs/current/auth-username-maps.html) between system roles and an existing database role.
### User Mapping {#module-services-postgres-authentication-user-mapping}
Assume that your app creates a role `admin` and you want the `root` user to be able to login with it.
You can then use [](#opt-services.postgresql.identMap) to define the map and [](#opt-services.postgresql.authentication) to enable it:
```nix
{
services.postgresql = {
identMap = ''
admin root admin
'';
authentication = ''
local all admin peer map=admin
'';
};
}
```
::: {.warning}
To avoid conflicts with other modules, you should never apply a map to `all` roles.
Because PostgreSQL will stop on the first matching line in `pg_hba.conf`, a line matching all roles would lock out other services.
Each module should only manage user maps for the database roles that belong to this module.
Best practice is to name the map after the database role it manages to avoid name conflicts.
:::
## Upgrading {#module-services-postgres-upgrading}
::: {.note}
The steps below demonstrate how to upgrade from an older version to `pkgs.postgresql_13`.
These instructions are also applicable to other versions.
:::
Major PostgreSQL upgrades require a downtime and a few imperative steps to be called. This is the case because
each major version has some internal changes in the databases' state. Because of that,
NixOS places the state into {file}`/var/lib/postgresql/&lt;version&gt;` where each `version`
can be obtained like this:
```
$ nix-instantiate --eval -A postgresql_13.psqlSchema
"13"
```
For an upgrade, a script like this can be used to simplify the process:
```nix
{
config,
lib,
pkgs,
...
}:
{
environment.systemPackages = [
(
let
# XXX specify the postgresql package you'd like to upgrade to.
# Do not forget to list the extensions you need.
newPostgres = pkgs.postgresql_13.withPackages (pp: [
# pp.plv8
]);
cfg = config.services.postgresql;
in
pkgs.writeScriptBin "upgrade-pg-cluster" ''
set -eux
# XXX it's perhaps advisable to stop all services that depend on postgresql
systemctl stop postgresql
export NEWDATA="/var/lib/postgresql/${newPostgres.psqlSchema}"
export NEWBIN="${newPostgres}/bin"
export OLDDATA="${cfg.dataDir}"
export OLDBIN="${cfg.finalPackage}/bin"
install -d -m 0700 -o postgres -g postgres "$NEWDATA"
cd "$NEWDATA"
sudo -u postgres "$NEWBIN/initdb" -D "$NEWDATA" ${lib.escapeShellArgs cfg.initdbArgs}
sudo -u postgres "$NEWBIN/pg_upgrade" \
--old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
--old-bindir "$OLDBIN" --new-bindir "$NEWBIN" \
"$@"
''
)
];
}
```
The upgrade process is:
1. Add the above to your {file}`configuration.nix` and rebuild. Alternatively, add that into a separate file and reference it in the `imports` list.
2. Login as root (`sudo su -`).
3. Run `upgrade-pg-cluster`. This will stop the old postgresql cluster, initialize a new one and migrate the old one to the new one. You may supply arguments like `--jobs 4` and `--link` to speedup the migration process. See <https://www.postgresql.org/docs/current/pgupgrade.html> for details.
4. Change the postgresql package in NixOS configuration to the one you were upgrading to via [](#opt-services.postgresql.package). Rebuild NixOS. This should start the new postgres version using the upgraded data directory and all services you stopped during the upgrade.
5. After the upgrade it's advisable to analyze the new cluster:
- For PostgreSQL ≥ 14, use the `vacuumdb` command printed by the upgrades script.
- For PostgreSQL < 14, run (as `su -l postgres` in the [](#opt-services.postgresql.dataDir), in this example {file}`/var/lib/postgresql/13`):
```
$ ./analyze_new_cluster.sh
```
::: {.warning}
The next step removes the old state-directory!
:::
```
$ ./delete_old_cluster.sh
```
## Versioning and End-of-Life {#module-services-postgres-versioning}
PostgreSQL's versioning policy is described [here](https://www.postgresql.org/support/versioning/). TLDR:
- Each major version is supported for 5 years.
- Every three months there will be a new minor release, containing bug and security fixes.
- For criticial/security fixes there could be more minor releases inbetween. This happens *very* infrequently.
- After five years, a final minor version is released. This usually happens in early November.
- After that a version is considered end-of-life (EOL).
- Around February each year is the first time an EOL-release will not have received regular updates anymore.
Technically, we'd not want to have EOL'ed packages in a stable NixOS release, which is to be supported until one month after the previous release. Thus, with NixOS' release schedule in May and November, the oldest PostgreSQL version in nixpkgs would have to be supported until December. It could be argued that a soon-to-be-EOL-ed version should thus be removed in May for the .05 release already. But since new security vulnerabilities are first disclosed in February of the following year, we agreed on keeping the oldest PostgreSQL major version around one more cycle in [#310580](https://github.com/NixOS/nixpkgs/pull/310580#discussion_r1597284693).
Thus, our release workflow is as follows:
- In May, `nixpkgs` packages the beta release for an upcoming major version. This is packaged for nixos-unstable only and will not be part of any stable NixOS release.
- In September/October the new major version will be released, replacing the beta package in nixos-unstable.
- In November the last minor version for the oldest major will be released.
- Both the current stable .05 release and nixos-unstable should be updated to the latest minor that will usually be released in November.
- This is relevant for people who need to use this major for as long as possible. In that case its desirable to be able to pin nixpkgs to a commit that still has it, at the latest minor available.
- In November, before branch-off for the .11 release and after the update to the latest minor, the EOL-ed major will be removed from nixos-unstable.
This leaves a small gap of a couple of weeks after the latest minor release and the end of our support window for the .05 release, in which there could be an emergency release to other major versions of PostgreSQL - but not the oldest major we have in that branch. In that case: If we can't trivially patch the issue, we will mark the package/version as insecure **immediately**.
## `pg_config` {#module-services-postgres-pg_config}
`pg_config` is not part of the `postgresql`-package itself.
It is available under `postgresql_<major>.pg_config` and `libpq.pg_config`.
Use the `pg_config` from the postgresql package you're using in your build.
Also, `pg_config` is a shell-script that replicates the behavior of the upstream `pg_config` and ensures at build-time that the output doesn't change.
This approach is done for the following reasons:
* By using a shell script, cross compilation of extensions is made easier.
* The separation allowed a massive reduction of the runtime closure's size.
Any attempts to move `pg_config` into `$dev` resulted in brittle and more complex solutions
(see commits [`0c47767`](https://github.com/NixOS/nixpkgs/commit/0c477676412564bd2d5dadc37cf245fe4259f4d9), [`435f51c`](https://github.com/NixOS/nixpkgs/commit/435f51c37faf74375134dfbd7c5a4560da2a9ea7)).
* `pg_config` is only needed to build extensions or in some exceptions for building client libraries linking to `libpq.so`.
If such a build works without `pg_config`, this is strictly preferable over adding `pg_config` to the build environment.
With the current approach it's now explicit that this is needed.
## Options {#module-services-postgres-options}
A complete list of options for the PostgreSQL module may be found [here](#opt-services.postgresql.enable).
## Plugins {#module-services-postgres-plugins}
The collection of plugins for each PostgreSQL version can be accessed with `.pkgs`. For example, for the `pkgs.postgresql_15` package, its plugin collection is accessed by `pkgs.postgresql_15.pkgs`:
```ShellSession
$ nix repl '<nixpkgs>'
Loading '<nixpkgs>'...
Added 10574 variables.
nix-repl> postgresql_15.pkgs.<TAB><TAB>
postgresql_15.pkgs.cstore_fdw postgresql_15.pkgs.pg_repack
postgresql_15.pkgs.pg_auto_failover postgresql_15.pkgs.pg_safeupdate
postgresql_15.pkgs.pg_bigm postgresql_15.pkgs.pg_similarity
postgresql_15.pkgs.pg_cron postgresql_15.pkgs.pg_topn
postgresql_15.pkgs.pg_hll postgresql_15.pkgs.pgjwt
postgresql_15.pkgs.pg_partman postgresql_15.pkgs.pgroonga
...
```
To add plugins via NixOS configuration, set `services.postgresql.extensions`:
```nix
{
services.postgresql.package = pkgs.postgresql_17;
services.postgresql.extensions =
ps: with ps; [
pg_repack
postgis
];
}
```
You can build a custom `postgresql-with-plugins` (to be used outside of NixOS) using the function `.withPackages`. For example, creating a custom PostgreSQL package in an overlay can look like this:
```nix
self: super: {
postgresql_custom = self.postgresql_17.withPackages (ps: [
ps.pg_repack
ps.postgis
]);
}
```
Here's a recipe on how to override a particular plugin through an overlay:
```nix
self: super: {
postgresql_15 = super.postgresql_15 // {
pkgs = super.postgresql_15.pkgs // {
pg_repack = super.postgresql_15.pkgs.pg_repack.overrideAttrs (_: {
name = "pg_repack-v20181024";
src = self.fetchzip {
url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
sha256 = "17k6hq9xaax87yz79j773qyigm4fwk8z4zh5cyp6z0sxnwfqxxw5";
};
});
};
};
}
```
## Procedural Languages {#module-services-postgres-pls}
PostgreSQL ships the additional procedural languages PL/Perl, PL/Python and PL/Tcl as extensions.
They are packaged as plugins and can be made available in the same way as external extensions:
```nix
{
services.postgresql.extensions =
ps: with ps; [
plperl
plpython3
pltcl
];
}
```
Each procedural language plugin provides a `.withPackages` helper to make language specific packages available at run-time.
For example, to make `python3Packages.base58` available:
```nix
{
services.postgresql.extensions =
pgps: with pgps; [ (plpython3.withPackages (pyps: with pyps; [ base58 ])) ];
}
```
This currently works for:
- `plperl` by re-using `perl.withPackages`
- `plpython3` by re-using `python3.withPackages`
- `plr` by exposing `rPackages`
- `pltcl` by exposing `tclPackages`
## JIT (Just-In-Time compilation) {#module-services-postgres-jit}
[JIT](https://www.postgresql.org/docs/current/jit-reason.html)-support in the PostgreSQL package
is disabled by default because of the ~600MiB closure-size increase from the LLVM dependency. It
can be optionally enabled in PostgreSQL with the following config option:
```nix
{ services.postgresql.enableJIT = true; }
```
This makes sure that the [`jit`](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-JIT)-setting
is set to `on` and a PostgreSQL package with JIT enabled is used. Further tweaking of the JIT compiler, e.g. setting a different
query cost threshold via [`jit_above_cost`](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-JIT-ABOVE-COST)
can be done manually via [`services.postgresql.settings`](#opt-services.postgresql.settings).
The attribute-names of JIT-enabled PostgreSQL packages are suffixed with `_jit`, i.e. for each `pkgs.postgresql`
(and `pkgs.postgresql_<major>`) in `nixpkgs` there's also a `pkgs.postgresql_jit` (and `pkgs.postgresql_<major>_jit`).
Alternatively, a JIT-enabled variant can be derived from a given `postgresql` package via `postgresql.withJIT`.
This is also useful if it's not clear which attribute from `nixpkgs` was originally used (e.g. when working with
[`config.services.postgresql.package`](#opt-services.postgresql.package) or if the package was modified via an
overlay) since all modifications are propagated to `withJIT`. I.e.
```nix
with import <nixpkgs> {
overlays = [
(self: super: {
postgresql = super.postgresql.overrideAttrs (_: {
pname = "foobar";
});
})
];
};
postgresql.withJIT.pname
```
evaluates to `"foobar"`.
## Service hardening {#module-services-postgres-hardening}
The service created by the [`postgresql`-module](#opt-services.postgresql.enable) uses
several common hardening options from `systemd`, most notably:
* Memory pages must not be both writable and executable (this only applies to non-JIT setups).
* A system call filter (see {manpage}`systemd.exec(5)` for details on `@system-service`).
* A stricter default UMask (`0027`).
* Only sockets of type `AF_INET`/`AF_INET6`/`AF_NETLINK`/`AF_UNIX` allowed.
* Restricted filesystem access (private `/tmp`, most of the file-system hierarchy is mounted read-only, only process directories in `/proc` that are owned by the same user).
* When using [`TABLESPACE`](https://www.postgresql.org/docs/current/manage-ag-tablespaces.html)s, make sure to add the filesystem paths to `ReadWritePaths` like this:
```nix
{
systemd.services.postgresql.serviceConfig.ReadWritePaths = [ "/path/to/tablespace/location" ];
}
```
The NixOS module also contains necessary adjustments for extensions from `nixpkgs`,
if these are enabled. If an extension or a postgresql feature from `nixpkgs` breaks
with hardening, it's considered a bug.
When using extensions that are not packaged in `nixpkgs`, hardening adjustments may
become necessary.
## Notable differences to upstream {#module-services-postgres-upstream-deviation}
- To avoid circular dependencies between default and -dev outputs, the output of the `pg_config` system view has been removed.

View File

@@ -0,0 +1,966 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
any
attrValues
concatMapStrings
concatStringsSep
const
elem
escapeShellArgs
filter
filterAttrs
getAttr
getName
hasPrefix
isString
literalExpression
mapAttrs
mapAttrsToList
mkAfter
mkBefore
mkDefault
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
mkRemovedOptionModule
mkRenamedOptionModule
optionalString
pipe
sortProperties
types
versionAtLeast
warn
;
cfg = config.services.postgresql;
toStr =
value:
if true == value then
"yes"
else if false == value then
"no"
else if isString value then
"'${lib.replaceStrings [ "'" ] [ "''" ] value}'"
else
builtins.toString value;
# The main PostgreSQL configuration file.
configFile = pkgs.writeTextDir "postgresql.conf" (
concatStringsSep "\n" (
mapAttrsToList (n: v: "${n} = ${toStr v}") (filterAttrs (const (x: x != null)) cfg.settings)
)
);
configFileCheck = pkgs.runCommand "postgresql-configfile-check" { } ''
${cfg.finalPackage}/bin/postgres -D${configFile} -C config_file >/dev/null
touch $out
'';
groupAccessAvailable = versionAtLeast cfg.finalPackage.version "11.0";
extensionNames = map getName cfg.finalPackage.installedExtensions;
extensionInstalled = extension: elem extension extensionNames;
in
{
imports = [
(mkRemovedOptionModule [
"services"
"postgresql"
"extraConfig"
] "Use services.postgresql.settings instead.")
(mkRemovedOptionModule [
"services"
"postgresql"
"recoveryConfig"
] "PostgreSQL v12+ doesn't support recovery.conf.")
(mkRenamedOptionModule
[ "services" "postgresql" "logLinePrefix" ]
[ "services" "postgresql" "settings" "log_line_prefix" ]
)
(mkRenamedOptionModule
[ "services" "postgresql" "port" ]
[ "services" "postgresql" "settings" "port" ]
)
(mkRenamedOptionModule
[ "services" "postgresql" "extraPlugins" ]
[ "services" "postgresql" "extensions" ]
)
];
###### interface
options = {
services.postgresql = {
enable = mkEnableOption "PostgreSQL Server";
enableJIT = mkEnableOption "JIT support";
package = mkOption {
type = types.package;
example = literalExpression "pkgs.postgresql_15";
defaultText = literalExpression ''
if versionAtLeast config.system.stateVersion "25.11" then
pkgs.postgresql_17
else if versionAtLeast config.system.stateVersion "24.11" then
pkgs.postgresql_16
else if versionAtLeast config.system.stateVersion "23.11" then
pkgs.postgresql_15
else if versionAtLeast config.system.stateVersion "22.05" then
pkgs.postgresql_14
else
pkgs.postgresql_13
'';
description = ''
The package being used by postgresql.
'';
};
finalPackage = mkOption {
type = types.package;
readOnly = true;
default =
let
# ensure that
# services.postgresql = {
# enableJIT = true;
# package = pkgs.postgresql_<major>;
# };
# works.
withJit = if cfg.enableJIT then cfg.package.withJIT else cfg.package.withoutJIT;
withJitAndPackages = if cfg.extensions == [ ] then withJit else withJit.withPackages cfg.extensions;
in
withJitAndPackages;
defaultText = "with config.services.postgresql; package.withPackages extensions";
description = ''
The postgresql package that will effectively be used in the system.
It consists of the base package with plugins applied to it.
'';
};
systemCallFilter = mkOption {
type = types.attrsOf (
types.coercedTo types.bool (enable: { inherit enable; }) (
types.submodule (
{ name, ... }:
{
options = {
enable = mkEnableOption "${name} in postgresql's syscall filter";
priority = mkOption {
default =
if hasPrefix "@" name then
500
else if hasPrefix "~@" name then
1000
else
1500;
defaultText = literalExpression ''
if hasPrefix "@" name then 500 else if hasPrefix "~@" name then 1000 else 1500
'';
type = types.int;
description = ''
Set the priority of the system call filter setting. Later declarations
override earlier ones, e.g.
```ini
[Service]
SystemCallFilter=~read write
SystemCallFilter=write
```
results in a service where _only_ `read` is not allowed.
The ordering in the unit file is controlled by this option: the higher
the number, the later it will be added to the filterset.
By default, depending on the prefix a priority is assigned: usually, call-groups
(starting with `@`) are used to allow/deny a larger set of syscalls and later
on single syscalls are configured for exceptions. Hence, syscall groups
and negative groups are placed before individual syscalls by default.
'';
};
};
}
)
)
);
defaultText = literalExpression ''
{
"@system-service" = true;
"~@privileged" = true;
"~@resources" = true;
}
'';
description = ''
Configures the syscall filter for `postgresql.service`. The keys are
declarations for `SystemCallFilter` as described in {manpage}`systemd.exec(5)`.
The value is a boolean: `true` adds the attribute name to the syscall filter-set,
`false` doesn't. This is done to allow downstream configurations to turn off
restrictions made here. E.g. with
```nix
{
services.postgresql.systemCallFilter."~@resources" = false;
}
```
it's possible to remove the restriction on `@resources` (keep in mind that
`@system-service` implies `@resources`).
As described in the section for [](#opt-services.postgresql.systemCallFilter._name_.priority),
the ordering matters. Hence, it's also possible to specify customizations with
```nix
{
services.postgresql.systemCallFilter = {
"foobar" = { enable = true; priority = 23; };
};
}
```
[](#opt-services.postgresql.systemCallFilter._name_.enable) is the flag whether
or not it will be added to the `SystemCallFilter` of `postgresql.service`.
Settings with a higher priority are added after filter settings with a lower
priority. Hence, syscall groups with a higher priority can discard declarations
with a lower priority.
By default, syscall groups (i.e. attribute names starting with `@`) are added
_before_ negated groups (i.e. `~@` as prefix) _before_ syscall names
and negations.
'';
};
checkConfig = mkOption {
type = types.bool;
default = true;
description = "Check the syntax of the configuration file at compile time";
};
dataDir = mkOption {
type = types.path;
defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
example = "/var/lib/postgresql/15";
description = ''
The data directory for PostgreSQL. If left as the default value
this directory will automatically be created before the PostgreSQL server starts, otherwise
the sysadmin is responsible for ensuring the directory exists with appropriate ownership
and permissions.
'';
};
authentication = mkOption {
type = types.lines;
default = "";
description = ''
Defines how users authenticate themselves to the server. See the
[PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
for details on the expected format of this option. By default,
peer based authentication will be used for users connecting
via the Unix socket, and md5 password authentication will be
used for users connecting via TCP. Any added rules will be
inserted above the default rules. If you'd like to replace the
default rules entirely, you can use `lib.mkForce` in your
module.
'';
};
identMap = mkOption {
type = types.lines;
default = "";
example = ''
map-name-0 system-username-0 database-username-0
map-name-1 system-username-1 database-username-1
'';
description = ''
Defines the mapping from system users to database users.
See the [auth doc](https://postgresql.org/docs/current/auth-username-maps.html).
There is a default map "postgres" which is used for local peer authentication
as the postgres superuser role.
For example, to allow the root user to login as the postgres superuser, add:
```
postgres root postgres
```
'';
};
initdbArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = [
"--data-checksums"
"--allow-group-access"
];
description = ''
Additional arguments passed to `initdb` during data dir
initialisation.
'';
};
initialScript = mkOption {
type = types.nullOr types.path;
default = null;
example = literalExpression ''
pkgs.writeText "init-sql-script" '''
alter user postgres with password 'myPassword';
''';'';
description = ''
A file containing SQL statements to execute on first startup.
'';
};
ensureDatabases = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Ensures that the specified databases exist.
This option will never delete existing databases, especially not when the value of this
option is changed. This means that databases created once through this option or
otherwise have to be removed manually.
'';
example = [
"gitea"
"nextcloud"
];
};
ensureUsers = mkOption {
type = types.listOf (
types.submodule {
options = {
name = mkOption {
type = types.str;
description = ''
Name of the user to ensure.
'';
};
ensureDBOwnership = mkOption {
type = types.bool;
default = false;
description = ''
Grants the user ownership to a database with the same name.
This database must be defined manually in
[](#opt-services.postgresql.ensureDatabases).
'';
};
ensureClauses = mkOption {
description = ''
An attrset of clauses to grant to the user. Under the hood this uses the
[ALTER USER syntax](https://www.postgresql.org/docs/current/sql-alteruser.html) for each attrName where
the attrValue is true in the attrSet:
`ALTER USER user.name WITH attrName`
'';
example = literalExpression ''
{
superuser = true;
createrole = true;
createdb = true;
}
'';
default = { };
defaultText = lib.literalMD ''
The default, `null`, means that the user created will have the default permissions assigned by PostgreSQL. Subsequent server starts will not set or unset the clause, so imperative changes are preserved.
'';
type = types.submodule {
options =
let
defaultText = lib.literalMD ''
`null`: do not set. For newly created roles, use PostgreSQL's default. For existing roles, do not touch this clause.
'';
in
{
superuser = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user, created by the ensureUser attr, superuser permissions. From the postgres docs:
A database superuser bypasses all permission checks,
except the right to log in. This is a dangerous privilege
and should not be used carelessly; it is best to do most
of your work as a role that is not a superuser. To create
a new database superuser, use CREATE ROLE name SUPERUSER.
You must do this as a role that is already a superuser.
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
createrole = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user, created by the ensureUser attr, createrole permissions. From the postgres docs:
A role must be explicitly given permission to create more
roles (except for superusers, since those bypass all
permission checks). To create such a role, use CREATE
ROLE name CREATEROLE. A role with CREATEROLE privilege
can alter and drop other roles, too, as well as grant or
revoke membership in them. However, to create, alter,
drop, or change membership of a superuser role, superuser
status is required; CREATEROLE is insufficient for that.
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
createdb = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user, created by the ensureUser attr, createdb permissions. From the postgres docs:
A role must be explicitly given permission to create
databases (except for superusers, since those bypass all
permission checks). To create such a role, use CREATE
ROLE name CREATEDB.
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
"inherit" = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user created inherit permissions. From the postgres docs:
A role is given permission to inherit the privileges of
roles it is a member of, by default. However, to create a
role without the permission, use CREATE ROLE name
NOINHERIT.
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
login = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user, created by the ensureUser attr, login permissions. From the postgres docs:
Only roles that have the LOGIN attribute can be used as
the initial role name for a database connection. A role
with the LOGIN attribute can be considered the same as a
database user. To create a role with login privilege,
use either:
CREATE ROLE name LOGIN; CREATE USER name;
(CREATE USER is equivalent to CREATE ROLE except that
CREATE USER includes LOGIN by default, while CREATE ROLE
does not.)
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
replication = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
A role must explicitly be given permission to initiate
streaming replication (except for superusers, since those
bypass all permission checks). A role used for streaming
replication must have LOGIN permission as well. To create
such a role, use CREATE ROLE name REPLICATION LOGIN.
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
bypassrls = mkOption {
type = types.nullOr types.bool;
description = ''
Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
A role must be explicitly given permission to bypass
every row-level security (RLS) policy (except for
superusers, since those bypass all permission checks). To
create such a role, use CREATE ROLE name BYPASSRLS as a
superuser.
More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
'';
default = null;
inherit defaultText;
};
};
};
};
};
}
);
default = [ ];
description = ''
Ensures that the specified users exist.
The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
same name only, and that without the need for a password.
This option will never delete existing users or remove DB ownership of databases
once granted with `ensureDBOwnership = true;`. This means that this must be
cleaned up manually when changing after changing the config in here.
'';
example = literalExpression ''
[
{
name = "nextcloud";
}
{
name = "superuser";
ensureDBOwnership = true;
}
]
'';
};
enableTCPIP = mkOption {
type = types.bool;
default = false;
description = ''
Whether PostgreSQL should listen on all network interfaces.
If disabled, the database can only be accessed via its Unix
domain socket or via TCP connections to localhost.
'';
};
extensions = mkOption {
type = with types; coercedTo (listOf path) (path: _ignorePg: path) (functionTo (listOf path));
default = _: [ ];
example = literalExpression "ps: with ps; [ postgis pg_repack ]";
description = ''
List of PostgreSQL extensions to install.
'';
};
settings = mkOption {
type =
with types;
submodule {
freeformType = attrsOf (oneOf [
bool
float
int
str
]);
options = {
shared_preload_libraries = mkOption {
type = nullOr (coercedTo (listOf str) (concatStringsSep ",") commas);
default = null;
example = literalExpression ''[ "auto_explain" "anon" ]'';
description = ''
List of libraries to be preloaded.
'';
};
log_line_prefix = mkOption {
type = types.str;
default = "[%p] ";
example = "%m [%p] ";
description = ''
A printf-style string that is output at the beginning of each log line.
Upstream default is `'%m [%p] '`, i.e. it includes the timestamp. We do
not include the timestamp, because journal has it anyway.
'';
};
port = mkOption {
type = types.port;
default = 5432;
description = ''
The port on which PostgreSQL listens.
'';
};
};
};
default = { };
description = ''
PostgreSQL configuration. Refer to
<https://www.postgresql.org/docs/current/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
for an overview of `postgresql.conf`.
::: {.note}
String values will automatically be enclosed in single quotes. Single quotes will be
escaped with two single quotes as described by the upstream documentation linked above.
:::
'';
example = literalExpression ''
{
log_connections = true;
log_statement = "all";
logging_collector = true;
log_disconnections = true;
log_destination = lib.mkForce "syslog";
}
'';
};
superUser = mkOption {
type = types.str;
default = "postgres";
internal = true;
readOnly = true;
description = ''
PostgreSQL superuser account to use for various operations. Internal since changing
this value would lead to breakage while setting up databases.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
warnings = (
let
unstableState =
if lib.hasInfix "beta" cfg.package.version then
"in beta"
else if lib.hasInfix "rc" cfg.package.version then
"a release candidate"
else
null;
in
lib.optional (unstableState != null)
"PostgreSQL ${lib.versions.major cfg.package.version} is currently ${unstableState}, and is not advised for use in production environments."
);
assertions = map (
{ name, ensureDBOwnership, ... }:
{
assertion = ensureDBOwnership -> elem name cfg.ensureDatabases;
message = ''
For each database user defined with `services.postgresql.ensureUsers` and
`ensureDBOwnership = true;`, a database with the same name must be defined
in `services.postgresql.ensureDatabases`.
Offender: ${name} has not been found among databases.
'';
}
) cfg.ensureUsers;
services.postgresql.settings = {
hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
log_destination = "stderr";
listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
jit = mkDefault (if cfg.enableJIT then "on" else "off");
};
services.postgresql.package =
let
mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
mkWarn =
ver:
warn ''
The postgresql package is not pinned and selected automatically by
`system.stateVersion`. Right now this is `pkgs.postgresql_${ver}`, the
oldest postgresql version available and thus the next that will be
removed when EOL on the next stable cycle.
See also https://endoflife.date/postgresql
'';
base =
# XXX Don't forget to keep `defaultText` of `services.postgresql.package` up to date!
if versionAtLeast config.system.stateVersion "25.11" then
pkgs.postgresql_17
else if versionAtLeast config.system.stateVersion "24.11" then
pkgs.postgresql_16
else if versionAtLeast config.system.stateVersion "23.11" then
pkgs.postgresql_15
else if versionAtLeast config.system.stateVersion "22.05" then
pkgs.postgresql_14
else if versionAtLeast config.system.stateVersion "21.11" then
mkWarn "13" pkgs.postgresql_13
else if versionAtLeast config.system.stateVersion "20.03" then
mkThrow "11"
else if versionAtLeast config.system.stateVersion "17.09" then
mkThrow "9_6"
else
mkThrow "9_5";
in
# Note: when changing the default, make it conditional on
# system.stateVersion to maintain compatibility with existing
# systems!
mkDefault (if cfg.enableJIT then base.withJIT else base);
services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
services.postgresql.authentication = mkMerge [
(mkBefore "# Generated file; do not edit!")
(mkAfter ''
# default value of services.postgresql.authentication
local all postgres peer map=postgres
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
'')
];
# The default allows to login with the same database username as the current system user.
# This is the default for peer authentication without a map, but needs to be made explicit
# once a map is used.
services.postgresql.identMap = mkAfter ''
postgres postgres postgres
'';
services.postgresql.systemCallFilter = mkMerge [
(mapAttrs (const mkDefault) {
"@system-service" = true;
"~@privileged" = true;
"~@resources" = true;
})
(mkIf (any extensionInstalled [ "plv8" ]) {
"@pkey" = true;
})
(mkIf (any extensionInstalled [ "citus" ]) {
"getpriority" = true;
"setpriority" = true;
})
];
users.users.postgres = {
name = "postgres";
uid = config.ids.uids.postgres;
group = "postgres";
description = "PostgreSQL server user";
home = "${cfg.dataDir}";
useDefaultShell = true;
};
users.groups.postgres.gid = config.ids.gids.postgres;
environment.systemPackages = [ cfg.finalPackage ];
environment.pathsToLink = [
"/share/postgresql"
];
system.checks = lib.optional (
cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform
) configFileCheck;
systemd.targets.postgresql = {
description = "PostgreSQL";
wantedBy = [ "multi-user.target" ];
requires = [
"postgresql.service"
"postgresql-setup.service"
];
};
systemd.services.postgresql = {
description = "PostgreSQL Server";
after = [ "network.target" ];
# To trigger the .target also on "systemctl start postgresql" as well as on
# restarts & stops.
# Please note that postgresql.service & postgresql.target binding to
# each other makes the Restart=always rule racy and results
# in sometimes the service not being restarted.
wants = [ "postgresql.target" ];
partOf = [ "postgresql.target" ];
environment.PGDATA = cfg.dataDir;
path = [ cfg.finalPackage ];
preStart = ''
if ! test -e ${cfg.dataDir}/PG_VERSION; then
# Cleanup the data directory.
rm -f ${cfg.dataDir}/*.conf
# Initialise the database.
initdb -U ${cfg.superUser} ${escapeShellArgs cfg.initdbArgs}
# See postStart!
touch "${cfg.dataDir}/.first_startup"
fi
ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
'';
serviceConfig = mkMerge [
{
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = "postgres";
Group = "postgres";
RuntimeDirectory = "postgresql";
Type = if versionAtLeast cfg.package.version "9.6" then "notify" else "simple";
# Shut down Postgres using SIGINT ("Fast Shutdown mode"). See
# https://www.postgresql.org/docs/current/server-shutdown.html
KillSignal = "SIGINT";
KillMode = "mixed";
# Give Postgres a decent amount of time to clean up after
# receiving systemd's SIGINT.
TimeoutSec = 120;
ExecStart = "${cfg.finalPackage}/bin/postgres";
Restart = "always";
# Hardening
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
MemoryDenyWriteExecute = lib.mkDefault (
cfg.settings.jit == "off" && (!any extensionInstalled [ "plv8" ])
);
NoNewPrivileges = true;
LockPersonality = true;
PrivateDevices = true;
PrivateMounts = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK" # used for network interface enumeration
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = pipe cfg.systemCallFilter [
(mapAttrsToList (name: v: v // { inherit name; }))
(filter (getAttr "enable"))
sortProperties
(map (getAttr "name"))
];
UMask = if groupAccessAvailable then "0027" else "0077";
}
(mkIf (cfg.dataDir != "/var/lib/postgresql/${cfg.package.psqlSchema}") {
# The user provides their own data directory
ReadWritePaths = [ cfg.dataDir ];
})
(mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
# Provision the default data directory
StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
})
];
unitConfig =
let
inherit (config.systemd.services.postgresql.serviceConfig) TimeoutSec;
maxTries = 5;
bufferSec = 5;
in
{
RequiresMountsFor = "${cfg.dataDir}";
# The max. time needed to perform `maxTries` start attempts of systemd
# plus a bit of buffer time (bufferSec) on top.
StartLimitIntervalSec = TimeoutSec * maxTries + bufferSec;
StartLimitBurst = maxTries;
};
};
systemd.services.postgresql-setup = {
description = "PostgreSQL Setup Scripts";
requires = [ "postgresql.service" ];
after = [ "postgresql.service" ];
serviceConfig = {
User = "postgres";
Group = "postgres";
Type = "oneshot";
RemainAfterExit = true;
};
path = [ cfg.finalPackage ];
environment.PGPORT = builtins.toString cfg.settings.port;
# Wait for PostgreSQL to be ready to accept connections.
script = ''
check-connection() {
psql -d postgres -v ON_ERROR_STOP=1 <<-' EOF'
SELECT pg_is_in_recovery() \gset
\if :pg_is_in_recovery
\i still-recovering
\endif
EOF
}
while ! check-connection 2> /dev/null; do
if ! systemctl is-active --quiet postgresql.service; then exit 1; fi
sleep 0.1
done
if test -e "${cfg.dataDir}/.first_startup"; then
${optionalString (cfg.initialScript != null) ''
psql -f "${cfg.initialScript}" -d postgres
''}
rm -f "${cfg.dataDir}/.first_startup"
fi
''
+ optionalString (cfg.ensureDatabases != [ ]) ''
${concatMapStrings (database: ''
psql -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || psql -tAc 'CREATE DATABASE "${database}"'
'') cfg.ensureDatabases}
''
+ ''
${concatMapStrings (
user:
let
dbOwnershipStmt = optionalString user.ensureDBOwnership ''psql -tAc 'ALTER DATABASE "${user.name}" OWNER TO "${user.name}";' '';
filteredClauses = filterAttrs (name: value: value != null) user.ensureClauses;
clauseSqlStatements = attrValues (mapAttrs (n: v: if v then n else "no${n}") filteredClauses);
userClauses = ''psql -tAc 'ALTER ROLE "${user.name}" ${concatStringsSep " " clauseSqlStatements}' '';
in
''
psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || psql -tAc 'CREATE USER "${user.name}"'
${userClauses}
${dbOwnershipStmt}
''
) cfg.ensureUsers}
'';
};
};
meta.doc = ./postgresql.md;
meta.maintainers = pkgs.postgresql.meta.maintainers;
}

View File

@@ -0,0 +1,315 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.postgrest;
# Turns an attrset of libpq connection params:
# {
# dbname = "postgres";
# user = "authenticator";
# }
# into a libpq connection string:
# dbname=postgres user=authenticator
db-uri = lib.pipe (cfg.settings.db-uri or { }) [
(lib.filterAttrs (_: v: v != null))
(lib.mapAttrsToList (k: v: "${k}=${v}"))
(lib.concatStringsSep " ")
];
# Writes a postgrest config file according to:
# https://hackage.haskell.org/package/configurator-0.3.0.0/docs/Data-Configurator.html
# Only a subset of the functionality is used by PostgREST.
configFile = lib.pipe (cfg.settings // { inherit db-uri; }) [
(lib.filterAttrs (_: v: v != null))
(lib.mapAttrs (
_: v:
if true == v then
"true"
else if false == v then
"false"
else if lib.isInt v then
toString v
else
"\"${lib.escape [ "\"" ] v}\""
))
(lib.mapAttrsToList (k: v: "${k} = ${v}"))
(lib.concatStringsSep "\n")
(pkgs.writeText "postgrest.conf")
];
in
{
meta = {
maintainers = with lib.maintainers; [ wolfgangwalther ];
};
options.services.postgrest = {
enable = lib.mkEnableOption "PostgREST";
pgpassFile = lib.mkOption {
type =
with lib.types;
nullOr (pathWith {
inStore = false;
absolute = true;
});
default = null;
example = "/run/keys/db_password";
description = ''
The password to authenticate to PostgreSQL with.
Not needed for peer or trust based authentication.
The file must be a valid `.pgpass` file as described in:
<https://www.postgresql.org/docs/current/libpq-pgpass.html>
In most cases, the following will be enough:
```
*:*:*:*:<password>
```
'';
};
jwtSecretFile = lib.mkOption {
type =
with lib.types;
nullOr (pathWith {
inStore = false;
absolute = true;
});
default = null;
example = "/run/keys/jwt_secret";
description = ''
The secret or JSON Web Key (JWK) (or set) used to decode JWT tokens clients provide for authentication.
For security the key must be at least 32 characters long.
If this parameter is not specified then PostgREST refuses authentication requests.
<https://docs.postgrest.org/en/stable/references/configuration.html#jwt-secret>
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType =
with lib.types;
attrsOf (oneOf [
bool
ints.unsigned
str
]);
options = {
admin-server-port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
description = ''
Specifies the port for the admin server, which can be used for healthchecks.
<https://docs.postgrest.org/en/stable/references/admin_server.html#admin-server>
'';
};
db-config = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Enables the in-database configuration.
<https://docs.postgrest.org/en/stable/references/configuration.html#in-database-configuration>
::: {.note}
This is enabled by default upstream, but disabled by default in this module.
:::
'';
};
db-uri = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
# This should not be used; use pgpassFile instead.
options.password = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
# This should not be used; use pgpassFile instead.
options.passfile = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
};
default = { };
description = ''
libpq connection parameters as documented in:
<https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>
::: {.note}
The `settings.db-uri.password` and `settings.db-uri.passfile` options are blocked.
Use [`pgpassFile`](#opt-services.postgrest.pgpassFile) instead.
:::
'';
example = lib.literalExpression ''
{
host = "localhost";
dbname = "postgres";
}
'';
};
# This should not be used; use jwtSecretFile instead.
jwt-secret = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
server-host = lib.mkOption {
type = with lib.types; nullOr str;
default = "127.0.0.1";
description = ''
Where to bind the PostgREST web server.
::: {.note}
The admin server will also bind here, but potentially exposes sensitive information.
Make sure you turn off the admin server, when opening this to the public.
<https://github.com/PostgREST/postgrest/issues/3956>
:::
'';
};
server-port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
example = 3000;
description = ''
The TCP port to bind the web server.
'';
};
server-unix-socket = lib.mkOption {
type = with lib.types; nullOr path;
default = "/run/postgrest/postgrest.sock";
description = ''
Unix domain socket where to bind the PostgREST web server.
'';
};
};
};
default = { };
description = ''
PostgREST configuration as documented in:
<https://docs.postgrest.org/en/stable/references/configuration.html#list-of-parameters>
`db-uri` is represented as an attribute set, see [`settings.db-uri`](#opt-services.postgrest.settings.db-uri)
::: {.note}
The `settings.jwt-secret` option is blocked.
Use [`jwtSecretFile`](#opt-services.postgrest.jwtSecretFile) instead.
:::
'';
example = lib.literalExpression ''
{
db-anon-role = "anon";
db-uri.dbname = "postgres";
"app.settings.custom" = "value";
}
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = (cfg.settings.server-port == null) != (cfg.settings.server-unix-socket == null);
message = ''
PostgREST can listen either on a TCP port or on a unix socket, but not both.
Please set one of `settings.server-port`](#opt-services.postgrest.jwtSecretFile) or `settings.server-unix-socket` to `null`.
<https://docs.postgrest.org/en/stable/references/configuration.html#server-unix-socket>
'';
}
];
warnings =
lib.optional (cfg.settings.admin-server-port != null && cfg.settings.server-host != "127.0.0.1")
"The PostgREST admin server is potentially listening on a public host. This may expose sensitive information via the `/config` endpoint.";
# Since we're using DynamicUser, we can't add the e.g. nginx user to
# a postgrest group, so the unix socket must be world-readable to make it useful.
services.postgrest.settings.server-unix-socket-mode = "666";
systemd.services.postgrest = {
description = "PostgREST";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
"postgresql.target"
];
serviceConfig = {
CacheDirectory = "postgrest";
CacheDirectoryMode = "0700";
Environment =
lib.optional (cfg.pgpassFile != null) "PGPASSFILE=%C/postgrest/pgpass"
++ lib.optional (cfg.jwtSecretFile != null) "PGRST_JWT_SECRET=@%d/jwt_secret";
LoadCredential =
lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}"
++ lib.optional (cfg.jwtSecretFile != null) "jwt_secret:${cfg.jwtSecretFile}";
Restart = "always";
RuntimeDirectory = "postgrest";
User = "postgrest";
# Hardening
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateIPC = true;
PrivateMounts = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "" ];
UMask = "0077";
};
# Copy the pgpass file to different location, to have it report mode 0400.
# Fixes: https://github.com/systemd/systemd/issues/29435
script = ''
if [ -f "$CREDENTIALS_DIRECTORY/pgpass" ]; then
cp -f "$CREDENTIALS_DIRECTORY/pgpass" "$CACHE_DIRECTORY/pgpass"
fi
exec ${lib.getExe pkgs.postgrest} ${configFile}
'';
};
};
}

View File

@@ -0,0 +1,575 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.redis;
mkValueString =
value:
if value == true then
"yes"
else if value == false then
"no"
else
lib.generators.mkValueStringDefault { } value;
redisConfig =
settings:
pkgs.writeText "redis.conf" (
lib.generators.toKeyValue {
listsAsDuplicateKeys = true;
mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " ";
} settings
);
redisName = name: "redis" + lib.optionalString (name != "") ("-" + name);
enabledServers = lib.filterAttrs (name: conf: conf.enable) config.services.redis.servers;
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"redis"
"user"
] "The redis module now is hardcoded to the redis user.")
(lib.mkRemovedOptionModule [
"services"
"redis"
"dbpath"
] "The redis module now uses /var/lib/redis as data directory.")
(lib.mkRemovedOptionModule [
"services"
"redis"
"dbFilename"
] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
(lib.mkRemovedOptionModule [
"services"
"redis"
"appendOnlyFilename"
] "This option was never used.")
(lib.mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
(lib.mkRemovedOptionModule [
"services"
"redis"
"extraConfig"
] "Use services.redis.servers.*.settings instead.")
(lib.mkRenamedOptionModule
[ "services" "redis" "enable" ]
[ "services" "redis" "servers" "" "enable" ]
)
(lib.mkRenamedOptionModule [ "services" "redis" "port" ] [ "services" "redis" "servers" "" "port" ])
(lib.mkRenamedOptionModule
[ "services" "redis" "openFirewall" ]
[ "services" "redis" "servers" "" "openFirewall" ]
)
(lib.mkRenamedOptionModule [ "services" "redis" "bind" ] [ "services" "redis" "servers" "" "bind" ])
(lib.mkRenamedOptionModule
[ "services" "redis" "unixSocket" ]
[ "services" "redis" "servers" "" "unixSocket" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "unixSocketPerm" ]
[ "services" "redis" "servers" "" "unixSocketPerm" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "logLevel" ]
[ "services" "redis" "servers" "" "logLevel" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "logfile" ]
[ "services" "redis" "servers" "" "logfile" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "syslog" ]
[ "services" "redis" "servers" "" "syslog" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "databases" ]
[ "services" "redis" "servers" "" "databases" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "maxclients" ]
[ "services" "redis" "servers" "" "maxclients" ]
)
(lib.mkRenamedOptionModule [ "services" "redis" "save" ] [ "services" "redis" "servers" "" "save" ])
(lib.mkRenamedOptionModule
[ "services" "redis" "slaveOf" ]
[ "services" "redis" "servers" "" "slaveOf" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "masterAuth" ]
[ "services" "redis" "servers" "" "masterAuth" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "requirePass" ]
[ "services" "redis" "servers" "" "requirePass" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "requirePassFile" ]
[ "services" "redis" "servers" "" "requirePassFile" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "appendOnly" ]
[ "services" "redis" "servers" "" "appendOnly" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "appendFsync" ]
[ "services" "redis" "servers" "" "appendFsync" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "slowLogLogSlowerThan" ]
[ "services" "redis" "servers" "" "slowLogLogSlowerThan" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "slowLogMaxLen" ]
[ "services" "redis" "servers" "" "slowLogMaxLen" ]
)
(lib.mkRenamedOptionModule
[ "services" "redis" "settings" ]
[ "services" "redis" "servers" "" "settings" ]
)
];
###### interface
options = {
services.redis = {
package = lib.mkPackageOption pkgs "redis" { };
vmOverCommit =
lib.mkEnableOption ''
set `vm.overcommit_memory` sysctl to 1
(Suggested for Background Saving: <https://redis.io/docs/get-started/faq/>)
''
// {
default = true;
};
servers = lib.mkOption {
type =
with lib.types;
attrsOf (
submodule (
{ config, name, ... }:
{
options = {
enable = lib.mkEnableOption "Redis server";
user = lib.mkOption {
type = types.str;
default = redisName name;
defaultText = lib.literalExpression ''
if name == "" then "redis" else "redis-''${name}"
'';
description = ''
User account under which this instance of redis-server runs.
::: {.note}
If left as the default value this user will automatically be
created on system activation, otherwise you are responsible for
ensuring the user exists before the redis service starts.
'';
};
group = lib.mkOption {
type = types.str;
default = config.user;
defaultText = lib.literalExpression "config.user";
description = ''
Group account under which this instance of redis-server runs.
::: {.note}
If left as the default value this group will automatically be
created on system activation, otherwise you are responsible for
ensuring the group exists before the redis service starts.
'';
};
port = lib.mkOption {
type = types.port;
default = if name == "" then 6379 else 0;
defaultText = lib.literalExpression ''if name == "" then 6379 else 0'';
description = ''
The TCP port to accept connections.
If port 0 is specified Redis will not listen on a TCP socket.
'';
};
openFirewall = lib.mkOption {
type = types.bool;
default = false;
description = ''
Whether to open ports in the firewall for the server.
'';
};
extraParams = lib.mkOption {
type = with types; listOf str;
default = [ ];
description = "Extra parameters to append to redis-server invocation";
example = [ "--sentinel" ];
};
bind = lib.mkOption {
type = with types; nullOr str;
default = "127.0.0.1";
description = ''
The IP interface to bind to.
`null` means "all interfaces".
'';
example = "192.0.2.1";
};
unixSocket = lib.mkOption {
type = with types; nullOr path;
default = "/run/${redisName name}/redis.sock";
defaultText = lib.literalExpression ''
if name == "" then "/run/redis/redis.sock" else "/run/redis-''${name}/redis.sock"
'';
description = "The path to the socket to bind to.";
};
unixSocketPerm = lib.mkOption {
type = types.int;
default = 660;
description = "Change permissions for the socket";
example = 600;
};
logLevel = lib.mkOption {
type = types.str;
default = "notice"; # debug, verbose, notice, warning
example = "debug";
description = "Specify the server verbosity level, options: debug, verbose, notice, warning.";
};
logfile = lib.mkOption {
type = types.str;
default = "/dev/null";
description = "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output.";
example = "/var/log/redis.log";
};
syslog = lib.mkOption {
type = types.bool;
default = true;
description = "Enable logging to the system logger.";
};
databases = lib.mkOption {
type = types.int;
default = 16;
description = "Set the number of databases.";
};
maxclients = lib.mkOption {
type = types.int;
default = 10000;
description = "Set the max number of connected clients at the same time.";
};
save = lib.mkOption {
type = with types; listOf (listOf int);
default = [
[
900
1
]
[
300
10
]
[
60
10000
]
];
description = ''
The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes.
If set to the empty list (`[]`) then RDB persistence will be disabled (useful if you are using AOF or don't want any persistence).
'';
};
slaveOf = lib.mkOption {
type =
with types;
nullOr (
submodule (
{ ... }:
{
options = {
ip = lib.mkOption {
type = str;
description = "IP of the Redis master";
example = "192.168.1.100";
};
port = lib.mkOption {
type = port;
description = "port of the Redis master";
default = 6379;
};
};
}
)
);
default = null;
description = "IP and port to which this redis instance acts as a slave.";
example = {
ip = "192.168.1.100";
port = 6379;
};
};
masterAuth = lib.mkOption {
type = with types; nullOr str;
default = null;
description = ''
If the master is password protected (using the requirePass configuration)
it is possible to tell the slave to authenticate before starting the replication synchronization
process, otherwise the master will refuse the slave request.
(STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)'';
};
requirePass = lib.mkOption {
type = with types; nullOr str;
default = null;
description = ''
Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
Use requirePassFile to store it outside of the nix store in a dedicated file.
'';
example = "letmein!";
};
requirePassFile = lib.mkOption {
type = with types; nullOr path;
default = null;
description = "File with password for the database.";
example = "/run/keys/redis-password";
};
appendOnly = lib.mkOption {
type = types.bool;
default = false;
description = "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence.";
};
appendFsync = lib.mkOption {
type = types.str;
default = "everysec"; # no, always, everysec
description = "How often to fsync the append-only log, options: no, always, everysec.";
};
slowLogLogSlowerThan = lib.mkOption {
type = types.int;
default = 10000;
description = "Log queries whose execution take longer than X in milliseconds.";
example = 1000;
};
slowLogMaxLen = lib.mkOption {
type = types.int;
default = 128;
description = "Maximum number of items to keep in slow log.";
};
settings = lib.mkOption {
# TODO: this should be converted to freeformType
type =
with types;
attrsOf (oneOf [
bool
int
str
(listOf str)
]);
default = { };
description = ''
Redis configuration. Refer to
<https://redis.io/topics/config>
for details on supported values.
'';
example = lib.literalExpression ''
{
loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
}
'';
};
};
config.settings = lib.mkMerge [
{
inherit (config)
port
logfile
databases
maxclients
appendOnly
;
daemonize = false;
supervised = "systemd";
loglevel = config.logLevel;
syslog-enabled = config.syslog;
save =
if config.save == [ ] then
''""'' # Disable saving with `save = ""`
else
map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") config.save;
dbfilename = "dump.rdb";
dir = "/var/lib/${redisName name}";
appendfsync = config.appendFsync;
slowlog-log-slower-than = config.slowLogLogSlowerThan;
slowlog-max-len = config.slowLogMaxLen;
}
(lib.mkIf (config.bind != null) { inherit (config) bind; })
(lib.mkIf (config.unixSocket != null) {
unixsocket = config.unixSocket;
unixsocketperm = toString config.unixSocketPerm;
})
(lib.mkIf (config.slaveOf != null) {
slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}";
})
(lib.mkIf (config.masterAuth != null) { masterauth = config.masterAuth; })
(lib.mkIf (config.requirePass != null) { requirepass = config.requirePass; })
];
}
)
);
description = "Configuration of multiple `redis-server` instances.";
default = { };
};
};
};
###### implementation
config = lib.mkIf (enabledServers != { }) {
assertions = lib.attrValues (
lib.mapAttrs (name: conf: {
assertion = conf.requirePass != null -> conf.requirePassFile == null;
message = ''
You can only set one services.redis.servers.${name}.requirePass
or services.redis.servers.${name}.requirePassFile
'';
}) enabledServers
);
boot.kernel.sysctl = lib.mkIf cfg.vmOverCommit {
"vm.overcommit_memory" = "1";
};
networking.firewall.allowedTCPPorts = lib.concatMap (
conf: lib.optional conf.openFirewall conf.port
) (lib.attrValues enabledServers);
environment.systemPackages = [ cfg.package ];
users.users = lib.mapAttrs' (
name: conf:
lib.nameValuePair (redisName name) {
description = "System user for the redis-server instance ${name}";
isSystemUser = true;
group = redisName name;
}
) enabledServers;
users.groups = lib.mapAttrs' (
name: conf:
lib.nameValuePair (redisName name) {
}
) enabledServers;
systemd.services = lib.mapAttrs' (
name: conf:
lib.nameValuePair (redisName name) {
description = "Redis Server - ${redisName name}";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/${
cfg.package.serverBin or "redis-server"
} /var/lib/${redisName name}/redis.conf ${lib.escapeShellArgs conf.extraParams}";
ExecStartPre =
"+"
+ pkgs.writeShellScript "${redisName name}-prep-conf" (
let
redisConfVar = "/var/lib/${redisName name}/redis.conf";
redisConfRun = "/run/${redisName name}/nixos.conf";
redisConfStore = redisConfig conf.settings;
in
''
touch "${redisConfVar}" "${redisConfRun}"
chown '${conf.user}':'${conf.group}' "${redisConfVar}" "${redisConfRun}"
chmod 0600 "${redisConfVar}" "${redisConfRun}"
if [ ! -s ${redisConfVar} ]; then
echo 'include "${redisConfRun}"' > "${redisConfVar}"
fi
echo 'include "${redisConfStore}"' > "${redisConfRun}"
${lib.optionalString (conf.requirePassFile != null) ''
{
echo -n "requirepass "
cat ${lib.escapeShellArg conf.requirePassFile}
} >> "${redisConfRun}"
''}
''
);
Type = "notify";
# User and group
User = conf.user;
Group = conf.group;
# Runtime directory and mode
RuntimeDirectory = redisName name;
RuntimeDirectoryMode = "0750";
# State directory and mode
StateDirectory = redisName name;
StateDirectoryMode = "0700";
# Access write directories
UMask = "0077";
# Capabilities
CapabilityBoundingSet = "";
# Security
NoNewPrivileges = true;
# Process Properties
LimitNOFILE = lib.mkDefault "${toString (conf.maxclients + 32)}";
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
LockPersonality = true;
# we need to disable MemoryDenyWriteExecute for keydb
MemoryDenyWriteExecute = cfg.package.pname != "keydb";
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
# System Call Filtering
SystemCallArchitectures = "native";
SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
};
}
) enabledServers;
};
}

View File

@@ -0,0 +1,112 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.rethinkdb;
rethinkdb = cfg.package;
in
{
###### interface
options = {
services.rethinkdb = {
enable = lib.mkEnableOption "RethinkDB server";
#package = lib.mkOption {
# default = pkgs.rethinkdb;
# description = "Which RethinkDB derivation to use.";
#};
user = lib.mkOption {
default = "rethinkdb";
description = "User account under which RethinkDB runs.";
};
group = lib.mkOption {
default = "rethinkdb";
description = "Group which rethinkdb user belongs to.";
};
dbpath = lib.mkOption {
default = "/var/db/rethinkdb";
description = "Location where RethinkDB stores its data, 1 data directory per instance.";
};
pidpath = lib.mkOption {
default = "/run/rethinkdb";
description = "Location where each instance's pid file is located.";
};
#cfgpath = lib.mkOption {
# default = "/etc/rethinkdb/instances.d";
# description = "Location where RethinkDB stores it config files, 1 config file per instance.";
#};
# TODO: currently not used by our implementation.
#instances = lib.mkOption {
# type = lib.types.attrsOf lib.types.str;
# default = {};
# description = "List of named RethinkDB instances in our cluster.";
#};
};
};
###### implementation
config = lib.mkIf config.services.rethinkdb.enable {
environment.systemPackages = [ rethinkdb ];
systemd.services.rethinkdb = {
description = "RethinkDB server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
# TODO: abstract away 'default', which is a per-instance directory name
# allowing end user of this nix module to provide multiple instances,
# and associated directory per instance
ExecStart = "${rethinkdb}/bin/rethinkdb -d ${cfg.dbpath}/default";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = cfg.user;
Group = cfg.group;
PIDFile = "${cfg.pidpath}/default.pid";
PermissionsStartOnly = true;
};
preStart = ''
if ! test -e ${cfg.dbpath}; then
install -d -m0755 -o ${cfg.user} -g ${cfg.group} ${cfg.dbpath}
install -d -m0755 -o ${cfg.user} -g ${cfg.group} ${cfg.dbpath}/default
chown -R ${cfg.user}:${cfg.group} ${cfg.dbpath}
fi
if ! test -e "${cfg.pidpath}/default.pid"; then
install -D -o ${cfg.user} -g ${cfg.group} /dev/null "${cfg.pidpath}/default.pid"
fi
'';
};
users.users.rethinkdb = lib.mkIf (cfg.user == "rethinkdb") {
name = "rethinkdb";
description = "RethinkDB server user";
isSystemUser = true;
};
users.groups = lib.optionalAttrs (cfg.group == "rethinkdb") (
lib.singleton {
name = "rethinkdb";
}
);
};
}

View File

@@ -0,0 +1,103 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.surrealdb;
in
{
options = {
services.surrealdb = {
enable = lib.mkEnableOption "SurrealDB, a scalable, distributed, collaborative, document-graph database, for the realtime web";
package = lib.mkPackageOption pkgs "surrealdb" { };
dbPath = lib.mkOption {
type = lib.types.str;
description = ''
The path that surrealdb will write data to. Use null for in-memory.
Can be one of "memory", "rocksdb://:path", "surrealkv://:path", "tikv://:addr", "fdb://:addr".
'';
default = "rocksdb:///var/lib/surrealdb/";
example = "memory";
};
host = lib.mkOption {
type = lib.types.str;
description = ''
The host that surrealdb will connect to.
'';
default = "127.0.0.1";
example = "127.0.0.1";
};
port = lib.mkOption {
type = lib.types.port;
description = ''
The port that surrealdb will connect to.
'';
default = 8000;
example = 8000;
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"--allow-all"
"--user"
"root"
"--pass"
"root"
];
description = ''
Specify a list of additional command line flags.
'';
};
};
};
config = lib.mkIf cfg.enable {
# Used to connect to the running service
environment.systemPackages = [ cfg.package ];
systemd.services.surrealdb = {
description = "A scalable, distributed, collaborative, document-graph database, for the realtime web";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/surreal start --bind ${cfg.host}:${toString cfg.port} ${lib.strings.concatStringsSep " " cfg.extraFlags} -- ${cfg.dbPath}";
DynamicUser = true;
Restart = "on-failure";
StateDirectory = "surrealdb";
CapabilityBoundingSet = "";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectClock = true;
ProtectProc = "noaccess";
ProcSubset = "pid";
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectHostname = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
RestrictNamespaces = true;
LockPersonality = true;
RemoveIPC = true;
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
};
};
}

View File

@@ -0,0 +1,42 @@
# TigerBeetle {#module-services-tigerbeetle}
*Source:* {file}`modules/services/databases/tigerbeetle.nix`
*Upstream documentation:* <https://docs.tigerbeetle.com/>
TigerBeetle is a distributed financial accounting database designed for mission critical safety and performance.
To enable TigerBeetle, add the following to your {file}`configuration.nix`:
```nix
{ services.tigerbeetle.enable = true; }
```
When first started, the TigerBeetle service will create its data file at {file}`/var/lib/tigerbeetle` unless the file already exists, in which case it will just use the existing file.
If you make changes to the configuration of TigerBeetle after its data file was already created (for example increasing the replica count), you may need to remove the existing file to avoid conflicts.
## Configuring {#module-services-tigerbeetle-configuring}
By default, TigerBeetle will only listen on a local interface.
To configure it to listen on a different interface (and to configure it to connect to other replicas, if you're creating more than one), you'll have to set the `addresses` option.
Note that the TigerBeetle module won't open any firewall ports automatically, so if you configure it to listen on an external interface, you'll need to ensure that connections can reach it:
```nix
{
services.tigerbeetle = {
enable = true;
addresses = [ "0.0.0.0:3001" ];
};
networking.firewall.allowedTCPPorts = [ 3001 ];
}
```
A complete list of options for TigerBeetle can be found [here](#opt-services.tigerbeetle.enable).
## Upgrading {#module-services-tigerbeetle-upgrading}
Usually, TigerBeetle's [upgrade process](https://docs.tigerbeetle.com/operating/upgrading) only requires replacing the binary used for the servers.
This is not directly possible with NixOS since the new binary will be located at a different place in the Nix store.
However, since TigerBeetle is managed through systemd on NixOS, the only action you need to take when upgrading is to make sure the version of TigerBeetle you're upgrading to supports upgrades from the version you're currently running.
This information will be on the [release notes](https://github.com/tigerbeetle/tigerbeetle/releases) for the version you're upgrading to.

View File

@@ -0,0 +1,137 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tigerbeetle;
in
{
meta = {
maintainers = with lib.maintainers; [ danielsidhion ];
doc = ./tigerbeetle.md;
buildDocsInSandbox = true;
};
options = {
services.tigerbeetle = with lib; {
enable = mkEnableOption "TigerBeetle server";
package = mkPackageOption pkgs "tigerbeetle" { };
clusterId = mkOption {
type = types.either types.ints.unsigned (types.strMatching "[0-9]+");
default = 0;
description = ''
The 128-bit cluster ID used to create the replica data file (if needed).
Since Nix only supports integers up to 64 bits, you need to pass a string to this if the cluster ID can't fit in 64 bits.
Otherwise, you can pass the cluster ID as either an integer or a string.
'';
};
replicaIndex = mkOption {
type = types.ints.unsigned;
default = 0;
description = ''
The index (starting at 0) of the replica in the cluster.
'';
};
replicaCount = mkOption {
type = types.ints.unsigned;
default = 1;
description = ''
The number of replicas participating in replication of the cluster.
'';
};
cacheGridSize = mkOption {
type = types.strMatching "[0-9]+(K|M|G)iB";
default = "1GiB";
description = ''
The grid cache size.
The grid cache acts like a page cache for TigerBeetle.
It is recommended to set this as large as possible.
'';
};
addresses = mkOption {
type = types.listOf types.nonEmptyStr;
default = [ "3001" ];
description = ''
The addresses of all replicas in the cluster.
This should be a list of IPv4/IPv6 addresses with port numbers.
Either the address or port number (but not both) may be omitted, in which case a default of 127.0.0.1 or 3001 will be used.
The first address in the list corresponds to the address for replica 0, the second address for replica 1, and so on.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions =
let
numAddresses = builtins.length cfg.addresses;
in
[
{
assertion = cfg.replicaIndex < cfg.replicaCount;
message = "the TigerBeetle replica index must fit the configured replica count";
}
{
assertion = cfg.replicaCount == numAddresses;
message =
if cfg.replicaCount < numAddresses then
"TigerBeetle must not have more addresses than the configured number of replicas"
else
"TigerBeetle must be configured with the addresses of all replicas";
}
];
systemd.services.tigerbeetle =
let
replicaDataPath = "/var/lib/tigerbeetle/${builtins.toString cfg.clusterId}_${builtins.toString cfg.replicaIndex}.tigerbeetle";
in
{
description = "TigerBeetle server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
if ! test -e "${replicaDataPath}"; then
${lib.getExe cfg.package} format --cluster="${builtins.toString cfg.clusterId}" --replica="${builtins.toString cfg.replicaIndex}" --replica-count="${builtins.toString cfg.replicaCount}" "${replicaDataPath}"
fi
'';
serviceConfig = {
DevicePolicy = "closed";
DynamicUser = true;
ExecStart = "${lib.getExe cfg.package} start --cache-grid=${cfg.cacheGridSize} --addresses=${lib.escapeShellArg (builtins.concatStringsSep "," cfg.addresses)} ${replicaDataPath}";
LockPersonality = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "noaccess";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
StateDirectory = "tigerbeetle";
StateDirectoryMode = 700;
Type = "exec";
};
};
environment.systemPackages = [ cfg.package ];
};
}

View File

@@ -0,0 +1,161 @@
{
config,
pkgs,
lib,
utils,
...
}:
let
inherit (lib)
escapeShellArgs
getBin
hasPrefix
literalExpression
mkBefore
mkEnableOption
mkIf
mkOption
mkPackageOption
optionalString
types
;
cfg = config.services.victorialogs;
startCLIList = [
"${cfg.package}/bin/victoria-logs"
"-storageDataPath=/var/lib/${cfg.stateDir}"
"-httpListenAddr=${cfg.listenAddress}"
]
++ lib.optionals (cfg.basicAuthUsername != null) [
"-httpAuth.username=${cfg.basicAuthUsername}"
]
++ lib.optionals (cfg.basicAuthPasswordFile != null) [
"-httpAuth.password=file://%d/basic_auth_password"
];
in
{
options.services.victorialogs = {
enable = mkEnableOption "VictoriaLogs is an open source user-friendly database for logs from VictoriaMetrics";
package = mkPackageOption pkgs "victorialogs" { };
listenAddress = mkOption {
default = ":9428";
type = types.str;
description = ''
TCP address to listen for incoming http requests.
'';
};
stateDir = mkOption {
type = types.str;
default = "victorialogs";
description = ''
Directory below `/var/lib` to store VictoriaLogs data.
This directory will be created automatically using systemd's StateDirectory mechanism.
'';
};
basicAuthUsername = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Basic Auth username used to protect VictoriaLogs instance by authorization
'';
};
basicAuthPasswordFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
File that contains the Basic Auth password used to protect VictoriaLogs instance by authorization
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = literalExpression ''
[
"-loggerLevel=WARN"
]
'';
description = ''
Extra options to pass to VictoriaLogs. See {command}`victoria-logs -help` for
possible options.
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.basicAuthUsername == null && cfg.basicAuthPasswordFile == null)
|| (cfg.basicAuthUsername != null && cfg.basicAuthPasswordFile != null);
message = "Both basicAuthUsername and basicAuthPasswordFile must be set together to enable basicAuth functionality, or neither should be set.";
}
];
systemd.services.victorialogs = {
description = "VictoriaLogs logs database";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitBurst = 5;
serviceConfig = {
ExecStart = lib.concatStringsSep " " [
(escapeShellArgs startCLIList)
(utils.escapeSystemdExecArgs cfg.extraOptions)
];
DynamicUser = true;
LoadCredential = lib.optional (
cfg.basicAuthPasswordFile != null
) "basic_auth_password:${cfg.basicAuthPasswordFile}";
RestartSec = 1;
Restart = "on-failure";
RuntimeDirectory = "victorialogs";
RuntimeDirectoryMode = "0700";
StateDirectory = cfg.stateDir;
StateDirectoryMode = "0700";
# Hardening
DeviceAllow = [ "/dev/null rw" ];
DevicePolicy = "strict";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
postStart =
let
bindAddr = (optionalString (hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
in
mkBefore ''
until ${getBin pkgs.curl}/bin/curl -s -o /dev/null http://${bindAddr}/ping; do
sleep 1;
done
'';
};
};
}

View File

@@ -0,0 +1,247 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
cfg = config.services.victoriametrics;
settingsFormat = pkgs.formats.yaml { };
startCLIList = [
"${cfg.package}/bin/victoria-metrics"
"-storageDataPath=/var/lib/${cfg.stateDir}"
"-httpListenAddr=${cfg.listenAddress}"
]
++ lib.optionals (cfg.retentionPeriod != null) [ "-retentionPeriod=${cfg.retentionPeriod}" ]
++ cfg.extraOptions;
prometheusConfigYml = checkedConfig (
settingsFormat.generate "prometheusConfig.yaml" cfg.prometheusConfig
);
checkedConfig =
file:
if cfg.checkConfig then
pkgs.runCommand "checked-config" { nativeBuildInputs = [ cfg.package ]; } ''
ln -s ${file} $out
${lib.escapeShellArgs startCLIList} -promscrape.config=${file} -dryRun
''
else
file;
in
{
options.services.victoriametrics = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable VictoriaMetrics in single-node mode.
VictoriaMetrics is a fast, cost-effective and scalable monitoring solution and time series database.
'';
};
package = mkPackageOption pkgs "victoriametrics" { };
listenAddress = mkOption {
default = ":8428";
type = types.str;
description = ''
TCP address to listen for incoming http requests.
'';
};
stateDir = mkOption {
type = types.str;
default = "victoriametrics";
description = ''
Directory below `/var/lib` to store VictoriaMetrics metrics data.
This directory will be created automatically using systemd's StateDirectory mechanism.
'';
};
retentionPeriod = mkOption {
type = types.nullOr types.str;
default = null;
example = "15d";
description = ''
How long to retain samples in storage.
The minimum retentionPeriod is 24h or 1d. See also -retentionFilter
The following optional suffixes are supported: s (second), h (hour), d (day), w (week), y (year).
If suffix isn't set, then the duration is counted in months (default 1)
'';
};
basicAuthUsername = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Basic Auth username used to protect VictoriaMetrics instance by authorization
'';
};
basicAuthPasswordFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = ''
File that contains the Basic Auth password used to protect VictoriaMetrics instance by authorization
'';
};
prometheusConfig = lib.mkOption {
type = lib.types.submodule { freeformType = settingsFormat.type; };
default = { };
example = literalExpression ''
{
scrape_configs = [
{
job_name = "postgres-exporter";
metrics_path = "/metrics";
static_configs = [
{
targets = ["1.2.3.4:9187"];
labels.type = "database";
}
];
}
{
job_name = "node-exporter";
metrics_path = "/metrics";
static_configs = [
{
targets = ["1.2.3.4:9100"];
labels.type = "node";
}
{
targets = ["5.6.7.8:9100"];
labels.type = "node";
}
];
}
];
}
'';
description = ''
Config for prometheus style metrics.
See the docs: <https://docs.victoriametrics.com/vmagent/#how-to-collect-metrics-in-prometheus-format>
for more information.
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = literalExpression ''
[
"-loggerLevel=WARN"
]
'';
description = ''
Extra options to pass to VictoriaMetrics. See the docs:
<https://docs.victoriametrics.com/single-server-victoriametrics/#list-of-command-line-flags>
or {command}`victoriametrics -help` for more information.
'';
};
checkConfig = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Check configuration.
If you use credentials stored in external files (`environmentFile`, etc),
they will not be visible and it will report errors, despite a correct configuration.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.basicAuthUsername == null && cfg.basicAuthPasswordFile == null)
|| (cfg.basicAuthUsername != null && cfg.basicAuthPasswordFile != null);
message = "Both basicAuthUsername and basicAuthPasswordFile must be set together to enable basicAuth functionality, or neither should be set.";
}
];
systemd.services.victoriametrics = {
description = "VictoriaMetrics time series database";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitBurst = 5;
serviceConfig = {
ExecStart = lib.escapeShellArgs (
startCLIList
++ lib.optionals (cfg.prometheusConfig != { }) [ "-promscrape.config=${prometheusConfigYml}" ]
++ lib.optional (cfg.basicAuthUsername != null) "-httpAuth.username=${cfg.basicAuthUsername}"
++ lib.optional (
cfg.basicAuthPasswordFile != null
) "-httpAuth.password=file://%d/basic_auth_password"
);
DynamicUser = true;
LoadCredential = lib.optionals (cfg.basicAuthPasswordFile != null) [
"basic_auth_password:${cfg.basicAuthPasswordFile}"
];
RestartSec = 1;
Restart = "on-failure";
RuntimeDirectory = "victoriametrics";
RuntimeDirectoryMode = "0700";
StateDirectory = cfg.stateDir;
StateDirectoryMode = "0700";
# Increase the limit to avoid errors like 'too many open files' when merging small parts
LimitNOFILE = 1048576;
# Hardening
DeviceAllow = [ "/dev/null rw" ];
DevicePolicy = "strict";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
postStart =
let
bindAddr =
(lib.optionalString (lib.hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
in
lib.mkBefore ''
until ${lib.getBin pkgs.curl}/bin/curl -s -o /dev/null http://${bindAddr}/ping; do
sleep 1;
done
'';
};
};
}

View File

@@ -0,0 +1,173 @@
{
config,
pkgs,
lib,
utils,
...
}:
let
inherit (lib)
escapeShellArgs
getBin
hasPrefix
literalExpression
mkBefore
mkEnableOption
mkIf
mkOption
mkPackageOption
optionalString
types
;
cfg = config.services.victoriatraces;
startCLIList = [
"${cfg.package}/bin/victoria-traces"
"-storageDataPath=/var/lib/${cfg.stateDir}"
"-httpListenAddr=${cfg.listenAddress}"
]
++ lib.optionals (cfg.basicAuthUsername != null) [
"-httpAuth.username=${cfg.basicAuthUsername}"
]
++ lib.optionals (cfg.basicAuthPasswordFile != null) [
"-httpAuth.password=file://%d/basic_auth_password"
];
in
{
options.services.victoriatraces = {
enable = mkEnableOption "VictoriaTraces is an open source distributed traces storage and query engine from VictoriaMetrics";
package = mkPackageOption pkgs "victoriatraces" { };
listenAddress = mkOption {
default = ":10428";
type = types.str;
description = ''
TCP address to listen for incoming http requests.
'';
};
stateDir = mkOption {
type = types.str;
default = "victoriatraces";
description = ''
Directory below `/var/lib` to store VictoriaTraces data.
This directory will be created automatically using systemd's StateDirectory mechanism.
'';
};
retentionPeriod = mkOption {
type = types.str;
default = "7d";
example = "30d";
description = ''
Retention period for trace data. Data older than retentionPeriod is automatically deleted.
'';
};
basicAuthUsername = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Basic Auth username used to protect VictoriaTraces instance by authorization
'';
};
basicAuthPasswordFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
File that contains the Basic Auth password used to protect VictoriaTraces instance by authorization
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = literalExpression ''
[
"-loggerLevel=WARN"
"-retention.maxDiskSpaceUsageBytes=1073741824"
]
'';
description = ''
Extra options to pass to VictoriaTraces. See {command}`victoria-traces -help` for
possible options.
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.basicAuthUsername == null && cfg.basicAuthPasswordFile == null)
|| (cfg.basicAuthUsername != null && cfg.basicAuthPasswordFile != null);
message = "Both basicAuthUsername and basicAuthPasswordFile must be set together to enable basicAuth functionality, or neither should be set.";
}
];
systemd.services.victoriatraces = {
description = "VictoriaTraces distributed traces database";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitBurst = 5;
serviceConfig = {
ExecStart = lib.concatStringsSep " " [
(escapeShellArgs (startCLIList ++ [ "-retentionPeriod=${cfg.retentionPeriod}" ]))
(utils.escapeSystemdExecArgs cfg.extraOptions)
];
DynamicUser = true;
LoadCredential = lib.optional (
cfg.basicAuthPasswordFile != null
) "basic_auth_password:${cfg.basicAuthPasswordFile}";
RestartSec = 1;
Restart = "on-failure";
RuntimeDirectory = "victoriatraces";
RuntimeDirectoryMode = "0700";
StateDirectory = cfg.stateDir;
StateDirectoryMode = "0700";
# Increase the limit to avoid errors like 'too many open files' when handling many trace spans
LimitNOFILE = 1048576;
# Hardening
DeviceAllow = [ "/dev/null rw" ];
DevicePolicy = "strict";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
postStart =
let
bindAddr = (optionalString (hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
in
mkBefore ''
until ${getBin pkgs.curl}/bin/curl -s -o /dev/null http://${bindAddr}/ping; do
sleep 1;
done
'';
};
};
}