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